mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Update timed combo, add CharacerBehavior trait
This commit is contained in:
parent
fe19698d52
commit
6fc94c22ba
@ -1,4 +1,7 @@
|
||||
use crate::comp::CharacterState;
|
||||
use crate::{
|
||||
comp::{CharacterState, ToolData},
|
||||
states::*,
|
||||
};
|
||||
use specs::{Component, DenseVecStorage, FlaggedStorage, HashMapStorage};
|
||||
use std::time::Duration;
|
||||
|
||||
@ -12,8 +15,9 @@ pub enum CharacterAbility {
|
||||
Roll,
|
||||
ChargeAttack,
|
||||
TimedCombo {
|
||||
/// Amount of energy required to use ability
|
||||
cost: i32,
|
||||
tool: ToolData,
|
||||
buildup_duration: Duration,
|
||||
recover_duration: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
@ -35,11 +39,11 @@ impl From<CharacterAbility> for CharacterState {
|
||||
CharacterAbility::BasicAttack {
|
||||
buildup_duration,
|
||||
recover_duration,
|
||||
} => CharacterState::BasicAttack {
|
||||
} => CharacterState::BasicAttack(basic_attack::State {
|
||||
exhausted: false,
|
||||
buildup_duration,
|
||||
recover_duration,
|
||||
},
|
||||
}),
|
||||
CharacterAbility::BasicBlock { .. } => CharacterState::BasicBlock {},
|
||||
CharacterAbility::Roll { .. } => CharacterState::Roll {
|
||||
remaining_duration: Duration::from_millis(600),
|
||||
@ -47,12 +51,18 @@ impl From<CharacterAbility> for CharacterState {
|
||||
CharacterAbility::ChargeAttack { .. } => CharacterState::ChargeAttack {
|
||||
remaining_duration: Duration::from_millis(600),
|
||||
},
|
||||
CharacterAbility::TimedCombo { .. } => CharacterState::TimedCombo {
|
||||
stage: 1,
|
||||
stage_time_active: Duration::default(),
|
||||
CharacterAbility::TimedCombo {
|
||||
tool,
|
||||
buildup_duration,
|
||||
recover_duration,
|
||||
} => CharacterState::TimedCombo(timed_combo::State {
|
||||
tool,
|
||||
buildup_duration,
|
||||
recover_duration,
|
||||
stage: 0,
|
||||
stage_exhausted: false,
|
||||
can_transition: false,
|
||||
},
|
||||
stage_time_active: Duration::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
comp::{Energy, Ori, Pos, ToolData, Vel},
|
||||
event::{LocalEvent, ServerEvent},
|
||||
states::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::{Component, FlaggedStorage, HashMapStorage, VecStorage};
|
||||
@ -33,15 +34,6 @@ pub enum CharacterState {
|
||||
tool: ToolData,
|
||||
},
|
||||
Glide,
|
||||
/// A basic attacking state
|
||||
BasicAttack {
|
||||
/// How long till the state deals damage
|
||||
buildup_duration: Duration,
|
||||
/// How long till the state remains after dealing damage
|
||||
recover_duration: Duration,
|
||||
/// Whether the attack can deal more damage
|
||||
exhausted: bool,
|
||||
},
|
||||
/// A basic blocking state
|
||||
BasicBlock,
|
||||
ChargeAttack {
|
||||
@ -52,18 +44,11 @@ pub enum CharacterState {
|
||||
/// How long the state has until exiting
|
||||
remaining_duration: Duration,
|
||||
},
|
||||
/// A basic attacking state
|
||||
BasicAttack(basic_attack::State),
|
||||
/// A three-stage attack where play must click at appropriate times
|
||||
/// to continue attack chain.
|
||||
TimedCombo {
|
||||
/// `int` denoting what stage (of 3) the attack is in.
|
||||
stage: i8,
|
||||
/// How long current stage has been active
|
||||
stage_time_active: Duration,
|
||||
/// Whether current stage has exhausted its attack
|
||||
stage_exhausted: bool,
|
||||
/// Whether player has clicked at the proper time to go to next stage
|
||||
can_transition: bool,
|
||||
},
|
||||
TimedCombo(timed_combo::State),
|
||||
/// A three-stage attack where each attack pushes player forward
|
||||
/// and successive attacks increase in damage, while player holds button.
|
||||
TripleStrike {
|
||||
@ -81,16 +66,19 @@ pub enum CharacterState {
|
||||
impl CharacterState {
|
||||
pub fn is_wield(&self) -> bool {
|
||||
match self {
|
||||
CharacterState::Wielding { .. } => true,
|
||||
CharacterState::BasicAttack { .. } => true,
|
||||
CharacterState::BasicBlock { .. } => true,
|
||||
CharacterState::Wielding { .. }
|
||||
| CharacterState::BasicAttack(_)
|
||||
| CharacterState::TimedCombo(_)
|
||||
| CharacterState::BasicBlock { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_attack(&self) -> bool {
|
||||
match self {
|
||||
CharacterState::BasicAttack { .. } | CharacterState::ChargeAttack { .. } => true,
|
||||
CharacterState::BasicAttack(_)
|
||||
| CharacterState::TimedCombo(_)
|
||||
| CharacterState::ChargeAttack { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,11 @@ impl ToolData {
|
||||
use ToolKind::*;
|
||||
|
||||
match self.kind {
|
||||
Sword(_) => vec![TimedCombo { cost: -150 }],
|
||||
Sword(_) => vec![TimedCombo {
|
||||
buildup_duration: Duration::from_millis(1000),
|
||||
recover_duration: Duration::from_millis(500),
|
||||
tool: *self,
|
||||
}],
|
||||
Axe => vec![BasicAttack {
|
||||
buildup_duration: Duration::from_millis(1000),
|
||||
recover_duration: Duration::from_millis(500),
|
||||
|
@ -1,42 +1,50 @@
|
||||
use crate::{
|
||||
comp::{Attacking, CharacterState, EnergySource, ItemKind::Tool, StateUpdate},
|
||||
states::utils::*,
|
||||
sys::character_behavior::JoinData,
|
||||
sys::character_behavior::*,
|
||||
};
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
|
||||
pub fn behavior(data: &JoinData) -> StateUpdate {
|
||||
let mut update = StateUpdate {
|
||||
pos: *data.pos,
|
||||
vel: *data.vel,
|
||||
ori: *data.ori,
|
||||
energy: *data.energy,
|
||||
character: *data.character,
|
||||
local_events: VecDeque::new(),
|
||||
server_events: VecDeque::new(),
|
||||
};
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
|
||||
pub struct State {
|
||||
/// How long until state should deal damage
|
||||
pub buildup_duration: Duration,
|
||||
/// How long the state has until exiting
|
||||
pub recover_duration: Duration,
|
||||
/// Whether the attack can deal more damage
|
||||
pub exhausted: bool,
|
||||
}
|
||||
|
||||
impl CharacterBehavior for State {
|
||||
fn behavior(&self, data: &JoinData) -> StateUpdate {
|
||||
let mut update = StateUpdate {
|
||||
pos: *data.pos,
|
||||
vel: *data.vel,
|
||||
ori: *data.ori,
|
||||
energy: *data.energy,
|
||||
character: *data.character,
|
||||
local_events: VecDeque::new(),
|
||||
server_events: VecDeque::new(),
|
||||
};
|
||||
|
||||
if let CharacterState::BasicAttack {
|
||||
exhausted,
|
||||
buildup_duration,
|
||||
recover_duration,
|
||||
} = data.character
|
||||
{
|
||||
let tool_kind = data.stats.equipment.main.as_ref().map(|i| i.kind);
|
||||
handle_move(data, &mut update);
|
||||
|
||||
if buildup_duration != &Duration::default() {
|
||||
// Build up window
|
||||
if self.buildup_duration != Duration::default() {
|
||||
// Start to swing
|
||||
update.character = CharacterState::BasicAttack {
|
||||
buildup_duration: buildup_duration
|
||||
update.character = CharacterState::BasicAttack(State {
|
||||
buildup_duration: self
|
||||
.buildup_duration
|
||||
.checked_sub(Duration::from_secs_f32(data.dt.0))
|
||||
.unwrap_or_default(),
|
||||
recover_duration: *recover_duration,
|
||||
recover_duration: self.recover_duration,
|
||||
exhausted: false,
|
||||
};
|
||||
} else if !*exhausted {
|
||||
});
|
||||
}
|
||||
// Hit attempt window
|
||||
else if !self.exhausted {
|
||||
// Swing hits
|
||||
if let Some(Tool(tool)) = tool_kind {
|
||||
if let Some(tool) = unwrap_tool_data(data) {
|
||||
data.updater.insert(data.entity, Attacking {
|
||||
weapon: Some(tool),
|
||||
applied: false,
|
||||
@ -44,31 +52,34 @@ pub fn behavior(data: &JoinData) -> StateUpdate {
|
||||
});
|
||||
}
|
||||
|
||||
update.character = CharacterState::BasicAttack {
|
||||
buildup_duration: *buildup_duration,
|
||||
recover_duration: *recover_duration,
|
||||
update.character = CharacterState::BasicAttack(State {
|
||||
buildup_duration: self.buildup_duration,
|
||||
recover_duration: self.recover_duration,
|
||||
exhausted: true,
|
||||
};
|
||||
} else if recover_duration != &Duration::default() {
|
||||
// Recover from swing
|
||||
update.character = CharacterState::BasicAttack {
|
||||
buildup_duration: *buildup_duration,
|
||||
recover_duration: recover_duration
|
||||
});
|
||||
}
|
||||
// Swing recovery window
|
||||
else if self.recover_duration != Duration::default() {
|
||||
update.character = CharacterState::BasicAttack(State {
|
||||
buildup_duration: self.buildup_duration,
|
||||
recover_duration: self
|
||||
.recover_duration
|
||||
.checked_sub(Duration::from_secs_f32(data.dt.0))
|
||||
.unwrap_or_default(),
|
||||
exhausted: true,
|
||||
}
|
||||
} else {
|
||||
// Done
|
||||
if let Some(Tool(tool)) = tool_kind {
|
||||
});
|
||||
}
|
||||
// Done
|
||||
else {
|
||||
if let Some(tool) = unwrap_tool_data(data) {
|
||||
update.character = CharacterState::Wielding { tool };
|
||||
// Make sure attack component is removed
|
||||
data.updater.remove::<Attacking>(data.entity);
|
||||
} else {
|
||||
update.character = CharacterState::Idle;
|
||||
}
|
||||
}
|
||||
|
||||
// More handling
|
||||
// Subtract energy on successful hit
|
||||
if let Some(attack) = data.attacking {
|
||||
if attack.applied && attack.hit_count > 0 {
|
||||
data.updater.remove::<Attacking>(data.entity);
|
||||
@ -76,9 +87,6 @@ pub fn behavior(data: &JoinData) -> StateUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
update
|
||||
} else {
|
||||
update.character = CharacterState::Idle {};
|
||||
update
|
||||
}
|
||||
}
|
||||
|
@ -1,98 +1,137 @@
|
||||
use crate::{
|
||||
comp::{Attacking, CharacterState, ItemKind::Tool, StateUpdate},
|
||||
comp::{Attacking, CharacterState, EnergySource, StateUpdate, ToolData},
|
||||
states::utils::*,
|
||||
sys::character_behavior::JoinData,
|
||||
sys::character_behavior::{CharacterBehavior, JoinData},
|
||||
};
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
|
||||
pub fn behavior(data: &JoinData) -> StateUpdate {
|
||||
let mut update = StateUpdate {
|
||||
pos: *data.pos,
|
||||
vel: *data.vel,
|
||||
ori: *data.ori,
|
||||
energy: *data.energy,
|
||||
character: *data.character,
|
||||
local_events: VecDeque::new(),
|
||||
server_events: VecDeque::new(),
|
||||
};
|
||||
|
||||
if let CharacterState::TimedCombo {
|
||||
stage,
|
||||
stage_time_active,
|
||||
stage_exhausted,
|
||||
can_transition,
|
||||
} = data.character
|
||||
{
|
||||
// Sorry adam, I don't want to fix this rn, check out basic_attack to
|
||||
// see how to do it
|
||||
//
|
||||
/*
|
||||
let mut new_can_transition = *can_transition;
|
||||
let mut new_stage_exhausted = *stage_exhausted;
|
||||
let new_stage_time_active = stage_time_active
|
||||
.checked_add(Duration::from_secs_f32(data.dt.0))
|
||||
.unwrap_or(Duration::default());
|
||||
|
||||
match stage {
|
||||
1 => {
|
||||
if new_stage_time_active > tool.attack_buildup_duration() {
|
||||
if !*stage_exhausted {
|
||||
// Try to deal damage
|
||||
data.updater.insert(data.entity, Attacking {
|
||||
weapon: Some(*tool),
|
||||
applied: false,
|
||||
hit_count: 0,
|
||||
});
|
||||
new_stage_exhausted = true;
|
||||
} else {
|
||||
// Make sure to remove Attacking component
|
||||
data.updater.remove::<Attacking>(data.entity);
|
||||
}
|
||||
|
||||
// Check if player has timed click right
|
||||
if data.inputs.primary.is_just_pressed() {
|
||||
println!("Can transition");
|
||||
new_can_transition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if new_stage_time_active > tool.attack_duration() {
|
||||
if new_can_transition {
|
||||
update.character = CharacterState::TimedCombo {
|
||||
tool: *tool,
|
||||
stage: 2,
|
||||
stage_time_active: Duration::default(),
|
||||
stage_exhausted: false,
|
||||
can_transition: false,
|
||||
}
|
||||
} else {
|
||||
println!("Failed");
|
||||
attempt_wield(data, &mut update);
|
||||
}
|
||||
} else {
|
||||
update.character = CharacterState::TimedCombo {
|
||||
tool: *tool,
|
||||
stage: 1,
|
||||
stage_time_active: new_stage_time_active,
|
||||
stage_exhausted: new_stage_exhausted,
|
||||
can_transition: new_can_transition,
|
||||
}
|
||||
}
|
||||
},
|
||||
2 => {
|
||||
println!("2");
|
||||
attempt_wield(data, &mut update);
|
||||
},
|
||||
3 => {
|
||||
println!("3");
|
||||
attempt_wield(data, &mut update);
|
||||
},
|
||||
_ => {
|
||||
// Should never get here.
|
||||
},
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
update
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
|
||||
pub struct State {
|
||||
/// Denotes what stage (of 3) the attack is in
|
||||
pub stage: i8,
|
||||
/// Whether current stage has exhausted its attack
|
||||
pub stage_exhausted: bool,
|
||||
/// How long state waits before it should deal damage
|
||||
pub buildup_duration: Duration,
|
||||
/// How long the state waits until exiting
|
||||
pub recover_duration: Duration,
|
||||
/// Tracks how long current stage has been active
|
||||
pub stage_time_active: Duration,
|
||||
/// `ToolData` to be sent to `Attacking` component
|
||||
pub tool: ToolData,
|
||||
}
|
||||
|
||||
impl CharacterBehavior for State {
|
||||
fn behavior(&self, data: &JoinData) -> StateUpdate {
|
||||
let mut update = StateUpdate {
|
||||
pos: *data.pos,
|
||||
vel: *data.vel,
|
||||
ori: *data.ori,
|
||||
energy: *data.energy,
|
||||
character: *data.character,
|
||||
local_events: VecDeque::new(),
|
||||
server_events: VecDeque::new(),
|
||||
};
|
||||
|
||||
let new_stage_time_active = self
|
||||
.stage_time_active
|
||||
.checked_add(Duration::from_secs_f32(data.dt.0))
|
||||
.unwrap_or(Duration::default());
|
||||
|
||||
println!("Stage {:?}", self.stage);
|
||||
|
||||
if self.stage < 3 {
|
||||
// Build up window
|
||||
if new_stage_time_active < self.buildup_duration {
|
||||
// If the player is pressing primary btn
|
||||
if data.inputs.primary.is_just_pressed() {
|
||||
// They failed, go back to `Wielding`
|
||||
update.character = CharacterState::Wielding { tool: self.tool };
|
||||
}
|
||||
// Keep updating
|
||||
else {
|
||||
update.character = CharacterState::TimedCombo(State {
|
||||
tool: self.tool,
|
||||
stage: self.stage,
|
||||
buildup_duration: self.buildup_duration,
|
||||
recover_duration: self.recover_duration,
|
||||
stage_exhausted: false,
|
||||
stage_time_active: new_stage_time_active,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Hit attempt window
|
||||
else if !self.stage_exhausted {
|
||||
// Swing hits
|
||||
data.updater.insert(data.entity, Attacking {
|
||||
weapon: Some(self.tool),
|
||||
applied: false,
|
||||
hit_count: 0,
|
||||
});
|
||||
|
||||
update.character = CharacterState::TimedCombo(State {
|
||||
tool: self.tool,
|
||||
stage: self.stage,
|
||||
buildup_duration: self.buildup_duration,
|
||||
recover_duration: self.recover_duration,
|
||||
stage_exhausted: true,
|
||||
stage_time_active: new_stage_time_active,
|
||||
});
|
||||
}
|
||||
// Swing recovery window
|
||||
else if new_stage_time_active
|
||||
< self
|
||||
.buildup_duration
|
||||
.checked_add(self.recover_duration)
|
||||
.unwrap_or(Duration::default())
|
||||
{
|
||||
// Try to transition to next stage
|
||||
if data.inputs.primary.is_just_pressed() {
|
||||
update.character = CharacterState::TimedCombo(State {
|
||||
tool: self.tool,
|
||||
stage: self.stage + 1,
|
||||
buildup_duration: self.buildup_duration,
|
||||
recover_duration: self.recover_duration,
|
||||
stage_exhausted: true,
|
||||
stage_time_active: Duration::default(),
|
||||
});
|
||||
}
|
||||
// Player didn't click this frame
|
||||
else {
|
||||
// Update state
|
||||
update.character = CharacterState::TimedCombo(State {
|
||||
tool: self.tool,
|
||||
stage: self.stage,
|
||||
buildup_duration: self.buildup_duration,
|
||||
recover_duration: self.recover_duration,
|
||||
stage_exhausted: true,
|
||||
stage_time_active: new_stage_time_active,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Stage expired but missed transition to next stage
|
||||
else {
|
||||
// Back to `Wielding`
|
||||
update.character = CharacterState::Wielding { tool: self.tool };
|
||||
// Make sure attack component is removed
|
||||
data.updater.remove::<Attacking>(data.entity);
|
||||
}
|
||||
}
|
||||
// Made three successful hits!
|
||||
else {
|
||||
// Back to `Wielding`
|
||||
update.character = CharacterState::Wielding { tool: self.tool };
|
||||
// Make sure attack component is removed
|
||||
data.updater.remove::<Attacking>(data.entity);
|
||||
}
|
||||
|
||||
// Subtract energy on successful hit
|
||||
if let Some(attack) = data.attacking {
|
||||
if attack.applied && attack.hit_count > 0 {
|
||||
data.updater.remove::<Attacking>(data.entity);
|
||||
update.energy.change_by(100, EnergySource::HitEnemy);
|
||||
}
|
||||
}
|
||||
|
||||
update
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
comp::{CharacterAbility, CharacterState, EnergySource, ItemKind::Tool, StateUpdate, ToolData},
|
||||
event::LocalEvent,
|
||||
states::*,
|
||||
sys::{character_behavior::JoinData, phys::GRAVITY},
|
||||
};
|
||||
use std::time::Duration;
|
||||
@ -236,3 +237,11 @@ pub fn attempt_dodge_ability(data: &JoinData, update: &mut StateUpdate) {
|
||||
update.character = ability.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_tool_data(data: &JoinData) -> Option<ToolData> {
|
||||
if let Some(Tool(tool)) = data.stats.equipment.main.as_ref().map(|i| i.kind) {
|
||||
Some(tool)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
comp::{
|
||||
AbilityPool, Attacking, Body, CharacterState, Controller, ControllerInputs, Energy,
|
||||
Mounting, Ori, PhysicsState, Pos, Stats, Vel,
|
||||
Mounting, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel,
|
||||
},
|
||||
event::{EventBus, LocalEvent, ServerEvent},
|
||||
state::DeltaTime,
|
||||
@ -13,6 +13,11 @@ use specs::{Entities, Entity, Join, LazyUpdate, Read, ReadStorage, System, Write
|
||||
|
||||
// use std::collections::VecDeque;
|
||||
|
||||
pub trait CharacterBehavior {
|
||||
fn behavior(&self, data: &JoinData) -> StateUpdate;
|
||||
// fn init(data: &JoinData) -> CharacterState;
|
||||
}
|
||||
|
||||
/// Read-Only Data sent from Character Behavior System to bahvior fn's
|
||||
pub struct JoinData<'a> {
|
||||
pub entity: Entity,
|
||||
@ -173,12 +178,12 @@ impl<'a> System<'a> for Sys {
|
||||
CharacterState::Roll { .. } => states::roll::behavior(&j),
|
||||
CharacterState::Wielding { .. } => states::wielding::behavior(&j),
|
||||
CharacterState::Equipping { .. } => states::equipping::behavior(&j),
|
||||
CharacterState::BasicAttack { .. } => states::basic_attack::behavior(&j),
|
||||
CharacterState::BasicBlock { .. } => states::basic_block::behavior(&j),
|
||||
CharacterState::ChargeAttack { .. } => states::charge_attack::behavior(&j),
|
||||
CharacterState::Sit { .. } => states::sit::behavior(&j),
|
||||
CharacterState::TripleStrike { .. } => states::triple_strike::behavior(&j),
|
||||
CharacterState::TimedCombo { .. } => states::timed_combo::behavior(&j),
|
||||
CharacterState::BasicAttack (state) => state.behavior(&j),
|
||||
CharacterState::TimedCombo(state) => state.behavior(&j),
|
||||
|
||||
// Do not use default match.
|
||||
// _ => StateUpdate {
|
||||
|
@ -454,7 +454,7 @@ impl FigureMgr {
|
||||
skeleton_attr,
|
||||
)
|
||||
},
|
||||
CharacterState::BasicAttack { .. } => {
|
||||
CharacterState::BasicAttack(_) => {
|
||||
anim::character::AttackAnimation::update_skeleton(
|
||||
&target_base,
|
||||
(active_tool_kind, time),
|
||||
@ -463,6 +463,22 @@ impl FigureMgr {
|
||||
skeleton_attr,
|
||||
)
|
||||
},
|
||||
CharacterState::TimedCombo(s) => match s.stage {
|
||||
0 | 2 => anim::character::AttackAnimation::update_skeleton(
|
||||
&target_base,
|
||||
(active_tool_kind, time),
|
||||
state.state_time,
|
||||
&mut state_animation_rate,
|
||||
skeleton_attr,
|
||||
),
|
||||
_ => anim::character::ChargeAnimation::update_skeleton(
|
||||
&target_base,
|
||||
(active_tool_kind, time),
|
||||
state.state_time,
|
||||
&mut state_animation_rate,
|
||||
skeleton_attr,
|
||||
),
|
||||
},
|
||||
CharacterState::BasicBlock { .. } => {
|
||||
anim::character::BlockIdleAnimation::update_skeleton(
|
||||
&CharacterSkeleton::new(),
|
||||
|
Loading…
Reference in New Issue
Block a user