Initial experimental WIP
This commit is contained in:
commit
ca690c2864
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
3690
Cargo.lock
generated
Normal file
3690
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "turtlers"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "turtlers"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.8", features = ["dynamic"] }
|
||||||
|
bevy-inspector-egui = "0.12.1"
|
||||||
|
bevy_prototype_lyon = "0.6"
|
||||||
|
bevy_tweening = "0.5.0"
|
||||||
|
|
||||||
|
# Enable a small amount of optimization in debug mode
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
143
src/bin/animation.rs
Normal file
143
src/bin/animation.rs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
//! Create and play an animation defined by code that operates on the `Transform` component.
|
||||||
|
|
||||||
|
use std::f32::consts::{FRAC_PI_2, PI};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.insert_resource(AmbientLight {
|
||||||
|
color: Color::WHITE,
|
||||||
|
brightness: 1.0,
|
||||||
|
})
|
||||||
|
.add_startup_system(setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
mut animations: ResMut<Assets<AnimationClip>>,
|
||||||
|
) {
|
||||||
|
// Camera
|
||||||
|
commands.spawn_bundle(Camera3dBundle {
|
||||||
|
transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// The animation API uses the `Name` component to target entities
|
||||||
|
let planet = Name::new("planet");
|
||||||
|
let orbit_controller = Name::new("orbit_controller");
|
||||||
|
let satellite = Name::new("satellite");
|
||||||
|
|
||||||
|
// Creating the animation
|
||||||
|
let mut animation = AnimationClip::default();
|
||||||
|
// A curve can modify a single part of a transform, here the translation
|
||||||
|
animation.add_curve_to_path(
|
||||||
|
EntityPath {
|
||||||
|
parts: vec![planet.clone()],
|
||||||
|
},
|
||||||
|
VariableCurve {
|
||||||
|
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
||||||
|
keyframes: Keyframes::Translation(vec![
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
Vec3::new(-1.0, 0.0, 1.0),
|
||||||
|
Vec3::new(-1.0, 0.0, -1.0),
|
||||||
|
Vec3::new(1.0, 0.0, -1.0),
|
||||||
|
// in case seamless looping is wanted, the last keyframe should
|
||||||
|
// be the same as the first one
|
||||||
|
Vec3::new(1.0, 0.0, 1.0),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Or it can modify the rotation of the transform.
|
||||||
|
// To find the entity to modify, the hierarchy will be traversed looking for
|
||||||
|
// an entity with the right name at each level
|
||||||
|
animation.add_curve_to_path(
|
||||||
|
EntityPath {
|
||||||
|
parts: vec![planet.clone(), orbit_controller.clone()],
|
||||||
|
},
|
||||||
|
VariableCurve {
|
||||||
|
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
||||||
|
keyframes: Keyframes::Rotation(vec![
|
||||||
|
Quat::from_axis_angle(Vec3::Y, 0.0),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, PI),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, 3.0 * FRAC_PI_2),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, 0.0),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// If a curve in an animation is shorter than the other, it will not repeat
|
||||||
|
// until all other curves are finished. In that case, another animation should
|
||||||
|
// be created for each part that would have a different duration / period
|
||||||
|
animation.add_curve_to_path(
|
||||||
|
EntityPath {
|
||||||
|
parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()],
|
||||||
|
},
|
||||||
|
VariableCurve {
|
||||||
|
keyframe_timestamps: vec![0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0],
|
||||||
|
keyframes: Keyframes::Scale(vec![
|
||||||
|
Vec3::splat(0.8),
|
||||||
|
Vec3::splat(1.2),
|
||||||
|
Vec3::splat(0.8),
|
||||||
|
Vec3::splat(1.2),
|
||||||
|
Vec3::splat(0.8),
|
||||||
|
Vec3::splat(1.2),
|
||||||
|
Vec3::splat(0.8),
|
||||||
|
Vec3::splat(1.2),
|
||||||
|
Vec3::splat(0.8),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// There can be more than one curve targeting the same entity path
|
||||||
|
animation.add_curve_to_path(
|
||||||
|
EntityPath {
|
||||||
|
parts: vec![planet.clone(), orbit_controller.clone(), satellite.clone()],
|
||||||
|
},
|
||||||
|
VariableCurve {
|
||||||
|
keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
|
||||||
|
keyframes: Keyframes::Rotation(vec![
|
||||||
|
Quat::from_axis_angle(Vec3::Y, 0.0),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, FRAC_PI_2),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, PI),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, 3.0 * FRAC_PI_2),
|
||||||
|
Quat::from_axis_angle(Vec3::Y, 0.0),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the animation player, and set it to repeat
|
||||||
|
let mut player = AnimationPlayer::default();
|
||||||
|
player.play(animations.add(animation)).repeat();
|
||||||
|
|
||||||
|
// Create the scene that will be animated
|
||||||
|
// First entity is the planet
|
||||||
|
commands
|
||||||
|
.spawn_bundle(PbrBundle {
|
||||||
|
mesh: meshes.add(Mesh::from(shape::Icosphere::default())),
|
||||||
|
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
// Add the Name component, and the animation player
|
||||||
|
.insert_bundle((planet, player))
|
||||||
|
.with_children(|p| {
|
||||||
|
// This entity is just used for animation, but doesn't display anything
|
||||||
|
p.spawn_bundle(SpatialBundle::default())
|
||||||
|
// Add the Name component
|
||||||
|
.insert(orbit_controller)
|
||||||
|
.with_children(|p| {
|
||||||
|
// The satellite, placed at a distance of the planet
|
||||||
|
p.spawn_bundle(PbrBundle {
|
||||||
|
transform: Transform::from_xyz(1.5, 0.0, 0.0),
|
||||||
|
mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })),
|
||||||
|
material: materials.add(Color::rgb(0.3, 0.9, 0.3).into()),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
// Add the Name component
|
||||||
|
.insert(satellite);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
12
src/debug.rs
Normal file
12
src/debug.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
use bevy::prelude::Plugin;
|
||||||
|
use bevy_inspector_egui::WorldInspectorPlugin;
|
||||||
|
|
||||||
|
pub struct DebugPlugin;
|
||||||
|
|
||||||
|
impl Plugin for DebugPlugin {
|
||||||
|
fn build(&self, app: &mut bevy::prelude::App) {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
app.add_plugin(WorldInspectorPlugin::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/main.rs
Normal file
26
src/main.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
mod debug;
|
||||||
|
mod turtle;
|
||||||
|
mod turtle_shapes;
|
||||||
|
use bevy::{prelude::*, window::close_on_esc};
|
||||||
|
|
||||||
|
use bevy_prototype_lyon::prelude::*;
|
||||||
|
use turtle::TurtlePlugin;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.insert_resource(Msaa { samples: 4 })
|
||||||
|
.insert_resource(ClearColor(Color::BEIGE))
|
||||||
|
.insert_resource(WindowDescriptor {
|
||||||
|
width: 400.0,
|
||||||
|
height: 400.0,
|
||||||
|
title: "Turtle Window".to_string(),
|
||||||
|
present_mode: bevy::window::PresentMode::AutoVsync,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_plugin(ShapePlugin)
|
||||||
|
.add_plugin(debug::DebugPlugin)
|
||||||
|
.add_plugin(TurtlePlugin)
|
||||||
|
.add_system(close_on_esc)
|
||||||
|
.run();
|
||||||
|
}
|
222
src/turtle.rs
Normal file
222
src/turtle.rs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
use std::{f32::consts::PI, time::Duration};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_prototype_lyon::{entity::ShapeBundle, prelude::*};
|
||||||
|
use bevy_tweening::{
|
||||||
|
lens::{TransformPositionLens, TransformRotateXLens, TransformRotateZLens, TransformScaleLens},
|
||||||
|
Animator, EaseFunction, Lens, Sequence, Tween, Tweenable, TweeningPlugin, TweeningType,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::turtle_shapes;
|
||||||
|
|
||||||
|
pub struct TurtlePlugin;
|
||||||
|
|
||||||
|
impl Plugin for TurtlePlugin {
|
||||||
|
fn build(&self, app: &mut bevy::prelude::App) {
|
||||||
|
app.add_plugin(TweeningPlugin)
|
||||||
|
.add_startup_system(setup)
|
||||||
|
.add_system(keypresses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct Turtle {
|
||||||
|
colors: Colors,
|
||||||
|
commands: TurtleCommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Turtle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
colors: Colors {
|
||||||
|
color: Color::DARK_GRAY,
|
||||||
|
fill_color: Color::BLACK,
|
||||||
|
},
|
||||||
|
commands: TurtleCommands(vec![
|
||||||
|
TurtleCommand::Forward(Length(100.)),
|
||||||
|
TurtleCommand::Left(Angle(90.)),
|
||||||
|
]), /*
|
||||||
|
shape: TurtleShape(GeometryBuilder::build_as(
|
||||||
|
&turtle_shapes::turtle(),
|
||||||
|
DrawMode::Outlined {
|
||||||
|
fill_mode: FillMode::color(Color::MIDNIGHT_BLUE),
|
||||||
|
outline_mode: StrokeMode::new(Color::BLACK, 1.0),
|
||||||
|
},
|
||||||
|
Default::default(),
|
||||||
|
)), */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Turtle {
|
||||||
|
pub fn set_color(&mut self, color: Color) {
|
||||||
|
self.colors.color = color;
|
||||||
|
}
|
||||||
|
pub fn set_fill_color(&mut self, color: Color) {
|
||||||
|
self.colors.fill_color = color;
|
||||||
|
}
|
||||||
|
pub fn get_colors(&self) -> &Colors {
|
||||||
|
&self.colors
|
||||||
|
}
|
||||||
|
pub fn forward(&mut self) -> &mut Self {
|
||||||
|
self.commands.0.push(TurtleCommand::Forward(Length(100.0)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct TurtleCommands(Vec<TurtleCommand>);
|
||||||
|
|
||||||
|
impl TurtleCommands {
|
||||||
|
fn generate_tweenable(&self) -> Sequence<Transform> {
|
||||||
|
let mut seq = Sequence::with_capacity(self.0.len());
|
||||||
|
for op in &self.0 {
|
||||||
|
match op {
|
||||||
|
TurtleCommand::Forward(Length(x)) => {
|
||||||
|
println!("Adding Forward");
|
||||||
|
seq = seq.then(Tween::new(
|
||||||
|
// Use a quadratic easing on both endpoints.
|
||||||
|
EaseFunction::QuadraticInOut,
|
||||||
|
// Loop animation back and forth.
|
||||||
|
TweeningType::Once,
|
||||||
|
// Animation time (one way only; for ping-pong it takes 2 seconds
|
||||||
|
// to come back to start).
|
||||||
|
Duration::from_secs(1),
|
||||||
|
// The lens gives access to the Transform component of the Entity,
|
||||||
|
// for the Animator to animate it. It also contains the start and
|
||||||
|
// end values respectively associated with the progress ratios 0. and 1.
|
||||||
|
TransformPositionLens {
|
||||||
|
start: Vec3::ZERO,
|
||||||
|
end: Vec3::new(*x as f32, 40., 0.),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
TurtleCommand::Backward(_) => todo!(),
|
||||||
|
TurtleCommand::Left(Angle(x)) => {
|
||||||
|
println!("Adding Left");
|
||||||
|
seq = seq.then(Tween::new(
|
||||||
|
// Use a quadratic easing on both endpoints.
|
||||||
|
EaseFunction::QuadraticInOut,
|
||||||
|
// Loop animation back and forth.
|
||||||
|
TweeningType::Once,
|
||||||
|
// Animation time (one way only; for ping-pong it takes 2 seconds
|
||||||
|
// to come back to start).
|
||||||
|
Duration::from_secs(1),
|
||||||
|
// The lens gives access to the Transform component of the Entity,
|
||||||
|
// for the Animator to animate it. It also contains the start and
|
||||||
|
// end values respectively associated with the progress ratios 0. and 1.
|
||||||
|
TransformRotateZLens {
|
||||||
|
start: *x as f32 * (PI / 180.),
|
||||||
|
end: -*x as f32 * (PI / 180.),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
TurtleCommand::Right(_) => todo!(),
|
||||||
|
TurtleCommand::PenUp => todo!(),
|
||||||
|
TurtleCommand::PenDown => todo!(),
|
||||||
|
TurtleCommand::Circle => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Component)]
|
||||||
|
pub struct TurtleShape;
|
||||||
|
|
||||||
|
#[derive(Clone, Component)]
|
||||||
|
pub struct Colors {
|
||||||
|
color: Color,
|
||||||
|
fill_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Length(f64);
|
||||||
|
pub struct Angle(f64);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
enum TurtleCommand {
|
||||||
|
Forward(Length),
|
||||||
|
Backward(Length),
|
||||||
|
Left(Angle),
|
||||||
|
Right(Angle),
|
||||||
|
PenUp,
|
||||||
|
PenDown,
|
||||||
|
Circle,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TurtleMoveLens {
|
||||||
|
start: Vec3,
|
||||||
|
end: Vec3,
|
||||||
|
}
|
||||||
|
fn setup(mut commands: Commands) {
|
||||||
|
let animator = Animator::new(Tween::new(
|
||||||
|
// Use a quadratic easing on both endpoints.
|
||||||
|
EaseFunction::QuadraticInOut,
|
||||||
|
// Loop animation back and forth.
|
||||||
|
TweeningType::PingPong,
|
||||||
|
// Animation time (one way only; for ping-pong it takes 2 seconds
|
||||||
|
// to come back to start).
|
||||||
|
Duration::from_secs(1),
|
||||||
|
// The lens gives access to the Transform component of the Entity,
|
||||||
|
// for the Animator to animate it. It also contains the start and
|
||||||
|
// end values respectively associated with the progress ratios 0. and 1.
|
||||||
|
TransformPositionLens {
|
||||||
|
start: Vec3::ZERO,
|
||||||
|
end: Vec3::new(40., 40., 0.),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
commands.spawn_bundle(Camera2dBundle::default());
|
||||||
|
commands
|
||||||
|
.spawn_bundle(Turtle::default())
|
||||||
|
.insert_bundle(GeometryBuilder::build_as(
|
||||||
|
&turtle_shapes::turtle(),
|
||||||
|
DrawMode::Outlined {
|
||||||
|
fill_mode: FillMode::color(Color::MIDNIGHT_BLUE),
|
||||||
|
outline_mode: StrokeMode::new(Color::BLACK, 1.0),
|
||||||
|
},
|
||||||
|
Default::default(),
|
||||||
|
))
|
||||||
|
.insert(animator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sprite is animated by changing its translation depending on the time that has passed since
|
||||||
|
/// the last frame.
|
||||||
|
fn keypresses(
|
||||||
|
//time: Res<Time>,
|
||||||
|
keys: Res<Input<KeyCode>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut qry: Query<&mut Animator<Transform>>,
|
||||||
|
tcmd: Query<&TurtleCommands>,
|
||||||
|
) {
|
||||||
|
if keys.just_pressed(KeyCode::W) {
|
||||||
|
let tcmd = tcmd.single();
|
||||||
|
let c = tcmd.generate_tweenable();
|
||||||
|
let mut shap = qry.single_mut();
|
||||||
|
shap.set_tweenable(c);
|
||||||
|
/* commands
|
||||||
|
.spawn_bundle(Turtle::default())
|
||||||
|
.insert_bundle(GeometryBuilder::build_as(
|
||||||
|
&turtle_shapes::turtle(),
|
||||||
|
DrawMode::Outlined {
|
||||||
|
fill_mode: FillMode::color(Color::RED),
|
||||||
|
outline_mode: StrokeMode::new(Color::BLACK, 1.0),
|
||||||
|
},
|
||||||
|
Transform::from_translation(Vec3::new(-100., 0., 0.)),
|
||||||
|
))
|
||||||
|
.insert(Animator::new(Tween::new(
|
||||||
|
// Use a quadratic easing on both endpoints.
|
||||||
|
EaseFunction::QuadraticInOut,
|
||||||
|
// Loop animation back and forth.
|
||||||
|
TweeningType::PingPong,
|
||||||
|
// Animation time (one way only; for ping-pong it takes 2 seconds
|
||||||
|
// to come back to start).
|
||||||
|
Duration::from_secs(1),
|
||||||
|
// The lens gives access to the Transform component of the Entity,
|
||||||
|
// for the Animator to animate it. It also contains the start and
|
||||||
|
// end values respectively associated with the progress ratios 0. and 1.
|
||||||
|
TransformPositionLens {
|
||||||
|
start: Vec3::new(-100., 0., 0.),
|
||||||
|
end: Vec3::new(-140., 40., 0.),
|
||||||
|
},
|
||||||
|
))); */
|
||||||
|
}
|
||||||
|
}
|
42
src/turtle_shapes.rs
Normal file
42
src/turtle_shapes.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use bevy::prelude::Vec2;
|
||||||
|
use bevy_prototype_lyon::prelude::{Path, PathBuilder};
|
||||||
|
|
||||||
|
pub fn turtle() -> Path {
|
||||||
|
let polygon = &[
|
||||||
|
[-2.5f32, 14.0f32],
|
||||||
|
[-1.25f32, 10.0f32],
|
||||||
|
[-4.0f32, 7.0f32],
|
||||||
|
[-7.0f32, 9.0f32],
|
||||||
|
[-9.0f32, 8.0f32],
|
||||||
|
[-6.0f32, 5.0f32],
|
||||||
|
[-7.0f32, 1.0f32],
|
||||||
|
[-5.0f32, -3.0f32],
|
||||||
|
[-8.0f32, -6.0f32],
|
||||||
|
[-6.0f32, -8.0f32],
|
||||||
|
[-4.0f32, -5.0f32],
|
||||||
|
[0.0f32, -7.0f32],
|
||||||
|
[4.0f32, -5.0f32],
|
||||||
|
[6.0f32, -8.0f32],
|
||||||
|
[8.0f32, -6.0f32],
|
||||||
|
[5.0f32, -3.0f32],
|
||||||
|
[7.0f32, 1.0f32],
|
||||||
|
[6.0f32, 5.0f32],
|
||||||
|
[9.0f32, 8.0f32],
|
||||||
|
[7.0f32, 9.0f32],
|
||||||
|
[4.0f32, 7.0f32],
|
||||||
|
[1.25f32, 10.0f32],
|
||||||
|
[2.5f32, 14.0f32],
|
||||||
|
];
|
||||||
|
let mut turtle_path = PathBuilder::new();
|
||||||
|
turtle_path.line_to(Vec2::new(1.0, 1.0));
|
||||||
|
turtle_path.line_to(Vec2::new(-1.0, 1.0));
|
||||||
|
turtle_path.line_to(Vec2::new(-1.0, -1.0));
|
||||||
|
turtle_path.line_to(Vec2::new(1.0, -1.0));
|
||||||
|
turtle_path.close();
|
||||||
|
turtle_path.move_to(Vec2::new(0.0, 16.0));
|
||||||
|
for coord in polygon {
|
||||||
|
turtle_path.line_to(Vec2::from_array(*coord));
|
||||||
|
}
|
||||||
|
turtle_path.close();
|
||||||
|
turtle_path.build()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user