Add buff reward + fix resolver bug
This commit is contained in:
parent
657ec807e0
commit
dbaf477f49
@ -1023,7 +1023,7 @@ impl DBTrans {
|
|||||||
WHERE \
|
WHERE \
|
||||||
(lower(details->>'display') LIKE $1) \
|
(lower(details->>'display') LIKE $1) \
|
||||||
OR EXISTS (SELECT 1 FROM jsonb_array_elements(aliases) AS al(alias) WHERE \
|
OR EXISTS (SELECT 1 FROM jsonb_array_elements(aliases) AS al(alias) WHERE \
|
||||||
LOWER(alias#>>'{{}}') LIKE $1)) {} \
|
LOWER(alias#>>'{{}}') LIKE $1) {} \
|
||||||
ORDER BY {} ABS(length(details->>'display')-$3) ASC \
|
ORDER BY {} ABS(length(details->>'display')-$3) ASC \
|
||||||
LIMIT $4 OFFSET $2",
|
LIMIT $4 OFFSET $2",
|
||||||
&cte_str, &extra_where, &extra_order
|
&cte_str, &extra_where, &extra_order
|
||||||
|
@ -151,6 +151,7 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
if wear_data.dodge_penalty != 0.0 {
|
if wear_data.dodge_penalty != 0.0 {
|
||||||
ctx.item.temporary_buffs.push(Buff {
|
ctx.item.temporary_buffs.push(Buff {
|
||||||
description: "Dodge penalty".to_owned(),
|
description: "Dodge penalty".to_owned(),
|
||||||
|
code: "dodge".to_owned(),
|
||||||
cause: BuffCause::ByItem {
|
cause: BuffCause::ByItem {
|
||||||
item_type: item_mut.item_type.clone(),
|
item_type: item_mut.item_type.clone(),
|
||||||
item_code: item_mut.item_code.clone(),
|
item_code: item_mut.item_code.clone(),
|
||||||
|
@ -33,12 +33,28 @@ pub enum BuffImpact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct Buff {
|
pub struct Buff {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
pub code: String,
|
||||||
pub cause: BuffCause,
|
pub cause: BuffCause,
|
||||||
pub impacts: Vec<BuffImpact>,
|
pub impacts: Vec<BuffImpact>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Buff {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
description: "Default".to_owned(),
|
||||||
|
code: "default".to_owned(),
|
||||||
|
cause: BuffCause::WaitingTask {
|
||||||
|
task_code: "default".to_owned(),
|
||||||
|
task_type: "default".to_owned(),
|
||||||
|
},
|
||||||
|
impacts: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum SkillType {
|
pub enum SkillType {
|
||||||
Appraise,
|
Appraise,
|
||||||
|
@ -10,6 +10,7 @@ pub enum JournalType {
|
|||||||
JoinedHackerClub,
|
JoinedHackerClub,
|
||||||
// Misc
|
// Misc
|
||||||
Died,
|
Died,
|
||||||
|
SharedWithPlayer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||||
|
@ -69,6 +69,10 @@ pub enum TaskDetails {
|
|||||||
target: String,
|
target: String,
|
||||||
effect_type: EffectType,
|
effect_type: EffectType,
|
||||||
},
|
},
|
||||||
|
ExpireBuff {
|
||||||
|
item: String,
|
||||||
|
code: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
impl TaskDetails {
|
impl TaskDetails {
|
||||||
pub fn name(self: &Self) -> &'static str {
|
pub fn name(self: &Self) -> &'static str {
|
||||||
@ -94,6 +98,7 @@ impl TaskDetails {
|
|||||||
ResetSpawns => "ResetSpawns",
|
ResetSpawns => "ResetSpawns",
|
||||||
ResetHanoi => "ResetHanoi",
|
ResetHanoi => "ResetHanoi",
|
||||||
HospitalERSeePatient { .. } => "HospitalERSeePatient",
|
HospitalERSeePatient { .. } => "HospitalERSeePatient",
|
||||||
|
ExpireBuff { .. } => "ExpireBuff",
|
||||||
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
|
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use crate::{
|
|||||||
listener::{ListenerMap, ListenerSend},
|
listener::{ListenerMap, ListenerSend},
|
||||||
message_handler::user_commands::{delete, drop, hire, open, rent},
|
message_handler::user_commands::{delete, drop, hire, open, rent},
|
||||||
models::task::Task,
|
models::task::Task,
|
||||||
services::{combat, effect, sharing, spawn, urges},
|
services::{combat, effect, sharing, spawn, tempbuff, urges},
|
||||||
static_content::{
|
static_content::{
|
||||||
npc::{self, computer_museum_npcs},
|
npc::{self, computer_museum_npcs},
|
||||||
room::general_hospital,
|
room::general_hospital,
|
||||||
@ -67,6 +67,7 @@ fn task_handler_registry(
|
|||||||
("ResetSpawns", spawn::RESET_SPAWNS_HANDLER),
|
("ResetSpawns", spawn::RESET_SPAWNS_HANDLER),
|
||||||
("ResetHanoi", computer_museum_npcs::RESET_GAME_HANDLER),
|
("ResetHanoi", computer_museum_npcs::RESET_GAME_HANDLER),
|
||||||
("HospitalERSeePatient", general_hospital::SEE_PATIENT_TASK),
|
("HospitalERSeePatient", general_hospital::SEE_PATIENT_TASK),
|
||||||
|
("ExpireBuff", tempbuff::EXPIRE_BUFF_TASK),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -24,6 +24,7 @@ pub mod effect;
|
|||||||
pub mod sharing;
|
pub mod sharing;
|
||||||
pub mod skills;
|
pub mod skills;
|
||||||
pub mod spawn;
|
pub mod spawn;
|
||||||
|
pub mod tempbuff;
|
||||||
pub mod urges;
|
pub mod urges;
|
||||||
|
|
||||||
pub fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
|
pub fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
|
||||||
|
@ -12,12 +12,15 @@ use crate::{
|
|||||||
models::{
|
models::{
|
||||||
consent::ConsentType,
|
consent::ConsentType,
|
||||||
item::{
|
item::{
|
||||||
ActiveConversation, ConversationIntensity, ConversationTopic,
|
ActiveConversation, Buff, BuffCause, BuffImpact, ConversationIntensity,
|
||||||
ConversationalInterestType, ConversationalStyle, Item, ItemFlag, SkillType,
|
ConversationTopic, ConversationalInterestType, ConversationalStyle, Item, ItemFlag,
|
||||||
|
SkillType, StatType,
|
||||||
},
|
},
|
||||||
|
journal::JournalType,
|
||||||
task::{Task, TaskDetails, TaskMeta},
|
task::{Task, TaskDetails, TaskMeta},
|
||||||
},
|
},
|
||||||
regular_tasks::{TaskHandler, TaskRunContext},
|
regular_tasks::{TaskHandler, TaskRunContext},
|
||||||
|
static_content::journals::award_journal_if_needed,
|
||||||
DResult,
|
DResult,
|
||||||
};
|
};
|
||||||
use ansi::ansi;
|
use ansi::ansi;
|
||||||
@ -302,6 +305,7 @@ impl TaskHandler for ShareTaskHandler {
|
|||||||
.or_insert(growth.max(0) as u64)
|
.or_insert(growth.max(0) as u64)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = compute_conversation_result(&p1, &p1_conv, &p2, &p2_conv);
|
let res = compute_conversation_result(&p1, &p1_conv, &p2, &p2_conv);
|
||||||
p1_mut.active_conversation.as_mut().map(|ac| {
|
p1_mut.active_conversation.as_mut().map(|ac| {
|
||||||
ac.peak_total_interest = res.my_total_interest.max(ac.peak_total_interest);
|
ac.peak_total_interest = res.my_total_interest.max(ac.peak_total_interest);
|
||||||
@ -408,7 +412,9 @@ async fn inform_player_convo_change_ready(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn share_skill_to_base_transition_time(skill: f64) -> f64 {
|
fn share_skill_to_base_transition_time(skill: f64) -> f64 {
|
||||||
((13.6666666666666666666667 - skill) * 3.0).max(1.0)
|
((13.6666666666666666666667 - skill) * 3.0)
|
||||||
|
.max(1.0)
|
||||||
|
.min(23.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_conversation_result(
|
fn compute_conversation_result(
|
||||||
@ -436,8 +442,8 @@ fn compute_conversation_result(
|
|||||||
let their_transition_time =
|
let their_transition_time =
|
||||||
share_skill_to_base_transition_time(their_theoretical_skill_level) as u64;
|
share_skill_to_base_transition_time(their_theoretical_skill_level) as u64;
|
||||||
|
|
||||||
let my_total_interest = my_direct_interest * their_transition_time;
|
let my_total_interest = my_direct_interest + 1363 * (their_transition_time - 1);
|
||||||
let their_total_interest = their_direct_interest * my_transition_time;
|
let their_total_interest = their_direct_interest + 1363 * (my_transition_time - 1);
|
||||||
|
|
||||||
ConversationResult {
|
ConversationResult {
|
||||||
my_total_interest,
|
my_total_interest,
|
||||||
@ -551,7 +557,7 @@ pub async fn start_conversation(
|
|||||||
if initiator.item_type != "player" {
|
if initiator.item_type != "player" {
|
||||||
user_error("Only players can initiate conversation with players".to_owned())?;
|
user_error("Only players can initiate conversation with players".to_owned())?;
|
||||||
}
|
}
|
||||||
let (_other_sess, _other_sessdat) =
|
let _ =
|
||||||
trans
|
trans
|
||||||
.find_session_for_player(&acceptor.item_code)
|
.find_session_for_player(&acceptor.item_code)
|
||||||
.await?
|
.await?
|
||||||
@ -670,6 +676,116 @@ pub async fn start_conversation(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn apply_conversation_buff(trans: &DBTrans, to_player: &mut Item) -> DResult<()> {
|
||||||
|
let peak = to_player
|
||||||
|
.active_conversation
|
||||||
|
.as_ref()
|
||||||
|
.map(|ac| ac.peak_total_interest)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let mut buffs: Vec<BuffImpact> = vec![];
|
||||||
|
const BASE: u64 = 7691;
|
||||||
|
if peak >= BASE * 7 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Cool,
|
||||||
|
magnitude: 2.0,
|
||||||
|
});
|
||||||
|
} else if peak >= BASE {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Cool,
|
||||||
|
magnitude: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if peak >= BASE * 8 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Brains,
|
||||||
|
magnitude: 2.0,
|
||||||
|
});
|
||||||
|
} else if peak >= BASE * 2 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Brains,
|
||||||
|
magnitude: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if peak >= BASE * 9 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Senses,
|
||||||
|
magnitude: 2.0,
|
||||||
|
});
|
||||||
|
} else if peak >= BASE * 3 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Senses,
|
||||||
|
magnitude: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if peak >= BASE * 10 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Endurance,
|
||||||
|
magnitude: 2.0,
|
||||||
|
});
|
||||||
|
} else if peak >= BASE * 4 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Endurance,
|
||||||
|
magnitude: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if peak >= BASE * 11 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Brawn,
|
||||||
|
magnitude: 2.0,
|
||||||
|
});
|
||||||
|
} else if peak >= BASE * 5 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Brawn,
|
||||||
|
magnitude: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if peak >= BASE * 12 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Reflexes,
|
||||||
|
magnitude: 2.0,
|
||||||
|
});
|
||||||
|
} else if peak >= BASE * 6 {
|
||||||
|
buffs.push(BuffImpact::ChangeStat {
|
||||||
|
stat: StatType::Reflexes,
|
||||||
|
magnitude: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let exp_task_code = format!("sharing/{}/{}", &to_player.item_type, &to_player.item_code);
|
||||||
|
|
||||||
|
to_player.temporary_buffs.retain(|b| match b.cause {
|
||||||
|
BuffCause::WaitingTask {
|
||||||
|
ref task_type,
|
||||||
|
ref task_code,
|
||||||
|
} if task_type == "ExpireBuff" && task_code == &exp_task_code => true,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
to_player.temporary_buffs.push(Buff {
|
||||||
|
description: "Sharing knowledge improved you (for a while)".to_owned(),
|
||||||
|
code: "sharing".to_owned(),
|
||||||
|
cause: BuffCause::WaitingTask {
|
||||||
|
task_type: "ExpireBuff".to_owned(),
|
||||||
|
task_code: exp_task_code.clone(),
|
||||||
|
},
|
||||||
|
impacts: buffs,
|
||||||
|
});
|
||||||
|
trans
|
||||||
|
.upsert_task(&Task {
|
||||||
|
meta: TaskMeta {
|
||||||
|
task_code: exp_task_code.clone(),
|
||||||
|
next_scheduled: Utc::now() + chrono::Duration::seconds(600),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
details: TaskDetails::ExpireBuff {
|
||||||
|
item: to_player.refstr(),
|
||||||
|
code: "sharing".to_owned(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn stop_conversation_mut(
|
pub async fn stop_conversation_mut(
|
||||||
trans: &DBTrans,
|
trans: &DBTrans,
|
||||||
participant: &mut Item,
|
participant: &mut Item,
|
||||||
@ -693,8 +809,39 @@ pub async fn stop_conversation_mut(
|
|||||||
};
|
};
|
||||||
let mut partner_mut = (*partner).clone();
|
let mut partner_mut = (*partner).clone();
|
||||||
|
|
||||||
|
apply_conversation_buff(trans, &mut partner_mut).await?;
|
||||||
|
apply_conversation_buff(trans, participant).await?;
|
||||||
|
|
||||||
participant.active_conversation = None;
|
participant.active_conversation = None;
|
||||||
partner_mut.active_conversation = None;
|
partner_mut.active_conversation = None;
|
||||||
|
|
||||||
|
if participant.item_type == "player" && partner_mut.item_type == "player" {
|
||||||
|
if let Some(mut participant_user) = trans.find_by_username(&participant.item_code).await? {
|
||||||
|
if award_journal_if_needed(
|
||||||
|
trans,
|
||||||
|
&mut participant_user,
|
||||||
|
participant,
|
||||||
|
JournalType::SharedWithPlayer,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
trans.save_user_model(&participant_user).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(mut partner_user) = trans.find_by_username(&partner.item_code).await? {
|
||||||
|
if award_journal_if_needed(
|
||||||
|
trans,
|
||||||
|
&mut partner_user,
|
||||||
|
&mut partner_mut,
|
||||||
|
JournalType::SharedWithPlayer,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
trans.save_user_model(&partner_user).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trans.save_item_model(&partner_mut).await?;
|
trans.save_item_model(&partner_mut).await?;
|
||||||
|
|
||||||
broadcast_to_room(
|
broadcast_to_room(
|
||||||
|
42
blastmud_game/src/services/tempbuff.rs
Normal file
42
blastmud_game/src/services/tempbuff.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use crate::{
|
||||||
|
models::task::TaskDetails,
|
||||||
|
regular_tasks::{TaskHandler, TaskRunContext},
|
||||||
|
DResult,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::time;
|
||||||
|
|
||||||
|
pub struct ExpireBuffTask;
|
||||||
|
#[async_trait]
|
||||||
|
impl TaskHandler for ExpireBuffTask {
|
||||||
|
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
||||||
|
let (item_ref, code) = match ctx.task.details {
|
||||||
|
TaskDetails::ExpireBuff { ref item, ref code } => Ok((item, code)),
|
||||||
|
_ => Err("Bad task type for ExpireBuffTask"),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let (item_type, item_code) = match item_ref.split_once("/") {
|
||||||
|
Some(v) => Ok(v),
|
||||||
|
None => Err("Invalid item for ExpireBuffTask"),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let item = ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code(item_type, item_code)
|
||||||
|
.await?;
|
||||||
|
let mut item = match item {
|
||||||
|
None => return Ok(None),
|
||||||
|
Some(i) => (*i).clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
item.temporary_buffs = item
|
||||||
|
.temporary_buffs
|
||||||
|
.into_iter()
|
||||||
|
.filter(|b| &b.code != code)
|
||||||
|
.collect();
|
||||||
|
ctx.trans.save_item_model(&item).await?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub static EXPIRE_BUFF_TASK: &'static (dyn TaskHandler + Sync + Send) = &ExpireBuffTask;
|
@ -61,7 +61,12 @@ pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
|
|||||||
name: "Carked it",
|
name: "Carked it",
|
||||||
details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.",
|
details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.",
|
||||||
xp: 150
|
xp: 150
|
||||||
})
|
}),
|
||||||
|
(JournalType::SharedWithPlayer, JournalData {
|
||||||
|
name: "Learned to share",
|
||||||
|
details: "sharing knowledge in a conversation [with another player]",
|
||||||
|
xp: 200,
|
||||||
|
}),
|
||||||
).into_iter().collect());
|
).into_iter().collect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,3 +22,9 @@ This is expected to be 0 - haven't seen any instances of it deviating, so any bu
|
|||||||
select i1.details->>'item_code' as i1_code, i1.details->>'active_combat' as i1_combat, i1.details->>'location' as i1_loc, i2.details->>'item_code' as i2_code, i2.details->>'active_combat' as i2_combat, i2.details->>'location' as i2_loc from items i1 join items i2 on i2.details->>'item_type' = (regexp_split_to_array(i1.details->'active_combat'->>'attacking', '/'))[1] and i2.details->>'item_code' = (regexp_split_to_array(i1.details->'active_combat'->>'attacking', '/'))[2] where not exists (select 1 from jsonb_array_elements_text(i2.details->'active_combat'->'attacked_by') e where e = ((i1.details->>'item_type') || '/' || (i1.details->>'item_code')));
|
select i1.details->>'item_code' as i1_code, i1.details->>'active_combat' as i1_combat, i1.details->>'location' as i1_loc, i2.details->>'item_code' as i2_code, i2.details->>'active_combat' as i2_combat, i2.details->>'location' as i2_loc from items i1 join items i2 on i2.details->>'item_type' = (regexp_split_to_array(i1.details->'active_combat'->>'attacking', '/'))[1] and i2.details->>'item_code' = (regexp_split_to_array(i1.details->'active_combat'->>'attacking', '/'))[2] where not exists (select 1 from jsonb_array_elements_text(i2.details->'active_combat'->'attacked_by') e where e = ((i1.details->>'item_type') || '/' || (i1.details->>'item_code')));
|
||||||
|
|
||||||
This should be empty, but there has been a bug breaking this.
|
This should be empty, but there has been a bug breaking this.
|
||||||
|
|
||||||
|
# Other types of migrations
|
||||||
|
## Rename a skill
|
||||||
|
|
||||||
|
Something like this:
|
||||||
|
with fexdet as (select item_id, jsonb_strip_nulls(jsonb_set(details, '{total_skills,Fuck}', 'null')) as details, details->'total_skills'->'Fuck' as f from items), amenddet as (select jsonb_set(details :: jsonb, '{total_skills,Share}', f :: jsonb) as details, item_id from fexdet where f is not null) update items i set details = a.details from amenddet a where i.item_id = a.item_id;
|
||||||
|
Loading…
Reference in New Issue
Block a user