mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Make more systems work with an optional health component, to allow disabling health on rtsim airships (so that players can't hammer them out of the sky).
This commit is contained in:
parent
e2a74c5e5c
commit
49f39fb752
@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- A system to add glow and reflection effects to figures (i.e: characters, armour, weapons, etc.)
|
- A system to add glow and reflection effects to figures (i.e: characters, armour, weapons, etc.)
|
||||||
- Merchants will trade wares with players
|
- Merchants will trade wares with players
|
||||||
- Airships that can be mounted and flown, and also walked on (`/airship` admin command)
|
- Airships that can be mounted and flown, and also walked on (`/airship` admin command)
|
||||||
|
- RtSim airships that fly between towns.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ pub enum ServerEvent {
|
|||||||
CreateNpc {
|
CreateNpc {
|
||||||
pos: comp::Pos,
|
pos: comp::Pos,
|
||||||
stats: comp::Stats,
|
stats: comp::Stats,
|
||||||
health: comp::Health,
|
health: Option<comp::Health>,
|
||||||
poise: comp::Poise,
|
poise: comp::Poise,
|
||||||
loadout: comp::inventory::loadout::Loadout,
|
loadout: comp::inventory::loadout::Loadout,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
|
@ -80,7 +80,7 @@ pub struct JoinData<'a> {
|
|||||||
pub dt: &'a DeltaTime,
|
pub dt: &'a DeltaTime,
|
||||||
pub controller: &'a Controller,
|
pub controller: &'a Controller,
|
||||||
pub inputs: &'a ControllerInputs,
|
pub inputs: &'a ControllerInputs,
|
||||||
pub health: &'a Health,
|
pub health: Option<&'a Health>,
|
||||||
pub energy: &'a Energy,
|
pub energy: &'a Energy,
|
||||||
pub inventory: &'a Inventory,
|
pub inventory: &'a Inventory,
|
||||||
pub body: &'a Body,
|
pub body: &'a Body,
|
||||||
@ -111,7 +111,7 @@ pub struct JoinStruct<'a> {
|
|||||||
pub energy: RestrictedMut<'a, Energy>,
|
pub energy: RestrictedMut<'a, Energy>,
|
||||||
pub inventory: RestrictedMut<'a, Inventory>,
|
pub inventory: RestrictedMut<'a, Inventory>,
|
||||||
pub controller: &'a mut Controller,
|
pub controller: &'a mut Controller,
|
||||||
pub health: &'a Health,
|
pub health: Option<&'a Health>,
|
||||||
pub body: &'a Body,
|
pub body: &'a Body,
|
||||||
pub physics: &'a PhysicsState,
|
pub physics: &'a PhysicsState,
|
||||||
pub melee_attack: Option<&'a Melee>,
|
pub melee_attack: Option<&'a Melee>,
|
||||||
|
@ -140,7 +140,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
&mut energies.restrict_mut(),
|
&mut energies.restrict_mut(),
|
||||||
&mut inventories.restrict_mut(),
|
&mut inventories.restrict_mut(),
|
||||||
&mut controllers,
|
&mut controllers,
|
||||||
&read_data.healths,
|
read_data.healths.maybe(),
|
||||||
&read_data.bodies,
|
&read_data.bodies,
|
||||||
&read_data.physics_states,
|
&read_data.physics_states,
|
||||||
&read_data.stats,
|
&read_data.stats,
|
||||||
@ -149,7 +149,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
.join()
|
.join()
|
||||||
{
|
{
|
||||||
// Being dead overrides all other states
|
// Being dead overrides all other states
|
||||||
if health.is_dead {
|
if health.map_or(false, |h| h.is_dead) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -248,7 +248,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
energy,
|
energy,
|
||||||
inventory,
|
inventory,
|
||||||
controller: &mut controller,
|
controller: &mut controller,
|
||||||
health: &health,
|
health,
|
||||||
body: &body,
|
body: &body,
|
||||||
physics: &physics,
|
physics: &physics,
|
||||||
melee_attack: read_data.melee_attacks.get(entity),
|
melee_attack: read_data.melee_attacks.get(entity),
|
||||||
|
@ -855,7 +855,7 @@ fn handle_spawn(
|
|||||||
id,
|
id,
|
||||||
npc::BodyType::from_body(body),
|
npc::BodyType::from_body(body),
|
||||||
)),
|
)),
|
||||||
comp::Health::new(body, 1),
|
Some(comp::Health::new(body, 1)),
|
||||||
comp::Poise::new(body),
|
comp::Poise::new(body),
|
||||||
inventory,
|
inventory,
|
||||||
body,
|
body,
|
||||||
@ -968,7 +968,14 @@ fn handle_spawn_training_dummy(
|
|||||||
|
|
||||||
server
|
server
|
||||||
.state
|
.state
|
||||||
.create_npc(pos, stats, health, poise, Inventory::new_empty(), body)
|
.create_npc(
|
||||||
|
pos,
|
||||||
|
stats,
|
||||||
|
Some(health),
|
||||||
|
poise,
|
||||||
|
Inventory::new_empty(),
|
||||||
|
body,
|
||||||
|
)
|
||||||
.with(comp::Vel(vel))
|
.with(comp::Vel(vel))
|
||||||
.with(comp::MountState::Unmounted)
|
.with(comp::MountState::Unmounted)
|
||||||
.build();
|
.build();
|
||||||
|
@ -48,7 +48,7 @@ pub fn handle_create_npc(
|
|||||||
server: &mut Server,
|
server: &mut Server,
|
||||||
pos: Pos,
|
pos: Pos,
|
||||||
stats: Stats,
|
stats: Stats,
|
||||||
health: Health,
|
health: Option<Health>,
|
||||||
poise: Poise,
|
poise: Poise,
|
||||||
loadout: Loadout,
|
loadout: Loadout,
|
||||||
body: Body,
|
body: Body,
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use common::{
|
use common::{
|
||||||
comp,
|
comp::{self, inventory::loadout_builder::LoadoutBuilder, ship},
|
||||||
comp::inventory::loadout_builder::LoadoutBuilder,
|
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
resources::{DeltaTime, Time},
|
resources::{DeltaTime, Time},
|
||||||
terrain::TerrainGrid,
|
terrain::TerrainGrid,
|
||||||
@ -106,7 +105,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
server_emitter.emit(ServerEvent::CreateNpc {
|
server_emitter.emit(ServerEvent::CreateNpc {
|
||||||
pos: comp::Pos(spawn_pos),
|
pos: comp::Pos(spawn_pos),
|
||||||
stats: comp::Stats::new(entity.get_name()),
|
stats: comp::Stats::new(entity.get_name()),
|
||||||
health: comp::Health::new(body, 10),
|
health: match body {
|
||||||
|
comp::Body::Ship(ship::Body::DefaultAirship) => None,
|
||||||
|
_ => Some(comp::Health::new(body, 10)),
|
||||||
|
},
|
||||||
loadout: match body {
|
loadout: match body {
|
||||||
comp::Body::Humanoid(_) => entity.get_loadout(),
|
comp::Body::Humanoid(_) => entity.get_loadout(),
|
||||||
_ => LoadoutBuilder::new().build(),
|
_ => LoadoutBuilder::new().build(),
|
||||||
|
@ -35,7 +35,7 @@ pub trait StateExt {
|
|||||||
&mut self,
|
&mut self,
|
||||||
pos: comp::Pos,
|
pos: comp::Pos,
|
||||||
stats: comp::Stats,
|
stats: comp::Stats,
|
||||||
health: comp::Health,
|
health: Option<comp::Health>,
|
||||||
poise: comp::Poise,
|
poise: comp::Poise,
|
||||||
inventory: comp::Inventory,
|
inventory: comp::Inventory,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
@ -162,12 +162,13 @@ impl StateExt for State {
|
|||||||
&mut self,
|
&mut self,
|
||||||
pos: comp::Pos,
|
pos: comp::Pos,
|
||||||
stats: comp::Stats,
|
stats: comp::Stats,
|
||||||
health: comp::Health,
|
health: Option<comp::Health>,
|
||||||
poise: comp::Poise,
|
poise: comp::Poise,
|
||||||
inventory: comp::Inventory,
|
inventory: comp::Inventory,
|
||||||
body: comp::Body,
|
body: comp::Body,
|
||||||
) -> EcsEntityBuilder {
|
) -> EcsEntityBuilder {
|
||||||
self.ecs_mut()
|
let mut res = self
|
||||||
|
.ecs_mut()
|
||||||
.create_entity_synced()
|
.create_entity_synced()
|
||||||
.with(pos)
|
.with(pos)
|
||||||
.with(comp::Vel(Vec3::zero()))
|
.with(comp::Vel(Vec3::zero()))
|
||||||
@ -199,9 +200,11 @@ impl StateExt for State {
|
|||||||
.unwrap_or(None)
|
.unwrap_or(None)
|
||||||
.unwrap_or(0),
|
.unwrap_or(0),
|
||||||
))
|
))
|
||||||
.with(stats)
|
.with(stats);
|
||||||
.with(health)
|
if let Some(health) = health {
|
||||||
.with(poise)
|
res = res.with(health);
|
||||||
|
}
|
||||||
|
res.with(poise)
|
||||||
.with(comp::Alignment::Npc)
|
.with(comp::Alignment::Npc)
|
||||||
.with(comp::Gravity(1.0))
|
.with(comp::Gravity(1.0))
|
||||||
.with(comp::CharacterState::default())
|
.with(comp::CharacterState::default())
|
||||||
|
@ -130,7 +130,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
job.cpu_stats.measure(ParMode::Rayon);
|
job.cpu_stats.measure(ParMode::Rayon);
|
||||||
(
|
(
|
||||||
&read_data.entities,
|
&read_data.entities,
|
||||||
(&read_data.energies, &read_data.healths),
|
(&read_data.energies, read_data.healths.maybe()),
|
||||||
&read_data.positions,
|
&read_data.positions,
|
||||||
&read_data.velocities,
|
&read_data.velocities,
|
||||||
&read_data.orientations,
|
&read_data.orientations,
|
||||||
@ -241,7 +241,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
let flees = alignment
|
let flees = alignment
|
||||||
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
|
.map(|a| !matches!(a, Alignment::Enemy | Alignment::Owned(_)))
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
let damage = health.current() as f32 / health.maximum() as f32;
|
let damage = health.map_or(1.0, |h| h.current() as f32 / h.maximum() as f32);
|
||||||
let rtsim_entity = read_data
|
let rtsim_entity = read_data
|
||||||
.rtsim_entities
|
.rtsim_entities
|
||||||
.get(entity)
|
.get(entity)
|
||||||
@ -394,8 +394,10 @@ impl<'a> System<'a> for Sys {
|
|||||||
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
|
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Target an entity that's attacking us if the attack was recent
|
// Target an entity that's attacking us if the attack was recent and we
|
||||||
if health.last_change.0 < DAMAGE_MEMORY_DURATION {
|
// have a health component
|
||||||
|
match health {
|
||||||
|
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
|
||||||
if let comp::HealthSource::Damage { by: Some(by), .. } =
|
if let comp::HealthSource::Damage { by: Some(by), .. } =
|
||||||
health.last_change.1.cause
|
health.last_change.1.cause
|
||||||
{
|
{
|
||||||
@ -403,7 +405,8 @@ impl<'a> System<'a> for Sys {
|
|||||||
read_data.uid_allocator.retrieve_entity_internal(by.id())
|
read_data.uid_allocator.retrieve_entity_internal(by.id())
|
||||||
{
|
{
|
||||||
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
if let Some(tgt_pos) = read_data.positions.get(attacker) {
|
||||||
// If the target is dead or in a safezone, remove the target
|
// If the target is dead or in a safezone, remove the
|
||||||
|
// target
|
||||||
// and idle.
|
// and idle.
|
||||||
if should_stop_attacking(
|
if should_stop_attacking(
|
||||||
read_data.healths.get(attacker),
|
read_data.healths.get(attacker),
|
||||||
@ -430,7 +433,9 @@ impl<'a> System<'a> for Sys {
|
|||||||
&read_data.dt,
|
&read_data.dt,
|
||||||
);
|
);
|
||||||
// Remember this encounter if an RtSim entity
|
// Remember this encounter if an RtSim entity
|
||||||
if let Some(tgt_stats) = read_data.stats.get(attacker) {
|
if let Some(tgt_stats) =
|
||||||
|
read_data.stats.get(attacker)
|
||||||
|
{
|
||||||
if data.rtsim_entity.is_some() {
|
if data.rtsim_entity.is_some() {
|
||||||
agent.rtsim_controller.events.push(
|
agent.rtsim_controller.events.push(
|
||||||
RtSimEvent::AddMemory(Memory {
|
RtSimEvent::AddMemory(Memory {
|
||||||
@ -456,10 +461,17 @@ impl<'a> System<'a> for Sys {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
agent.target = None;
|
agent.target = None;
|
||||||
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
|
data.idle_tree(
|
||||||
|
agent,
|
||||||
|
controller,
|
||||||
|
&read_data,
|
||||||
|
&mut event_emitter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
|
_ => {
|
||||||
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
|
data.idle_tree(agent, controller, &read_data, &mut event_emitter);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
server_emitter.emit(ServerEvent::CreateNpc {
|
server_emitter.emit(ServerEvent::CreateNpc {
|
||||||
pos: Pos(entity.pos),
|
pos: Pos(entity.pos),
|
||||||
stats,
|
stats,
|
||||||
health,
|
health: Some(health),
|
||||||
poise,
|
poise,
|
||||||
loadout,
|
loadout,
|
||||||
agent: if entity.has_agency {
|
agent: if entity.has_agency {
|
||||||
|
@ -1339,7 +1339,7 @@ impl Hud {
|
|||||||
&pos,
|
&pos,
|
||||||
interpolated.maybe(),
|
interpolated.maybe(),
|
||||||
&stats,
|
&stats,
|
||||||
&healths,
|
healths.maybe(),
|
||||||
&buffs,
|
&buffs,
|
||||||
energy.maybe(),
|
energy.maybe(),
|
||||||
scales.maybe(),
|
scales.maybe(),
|
||||||
@ -1352,7 +1352,7 @@ impl Hud {
|
|||||||
.filter(|t| {
|
.filter(|t| {
|
||||||
let health = t.4;
|
let health = t.4;
|
||||||
let entity = t.0;
|
let entity = t.0;
|
||||||
entity != me && !health.is_dead
|
entity != me && !health.map_or(false, |h| h.is_dead)
|
||||||
})
|
})
|
||||||
.filter_map(
|
.filter_map(
|
||||||
|(
|
|(
|
||||||
@ -1380,7 +1380,7 @@ impl Hud {
|
|||||||
let display_overhead_info =
|
let display_overhead_info =
|
||||||
(info.target_entity.map_or(false, |e| e == entity)
|
(info.target_entity.map_or(false, |e| e == entity)
|
||||||
|| info.selected_entity.map_or(false, |s| s.0 == entity)
|
|| info.selected_entity.map_or(false, |s| s.0 == entity)
|
||||||
|| overhead::should_show_healthbar(health)
|
|| health.map_or(true, overhead::should_show_healthbar)
|
||||||
|| in_group)
|
|| in_group)
|
||||||
&& dist_sqr
|
&& dist_sqr
|
||||||
< (if in_group {
|
< (if in_group {
|
||||||
@ -1400,9 +1400,9 @@ impl Hud {
|
|||||||
health,
|
health,
|
||||||
buffs,
|
buffs,
|
||||||
energy,
|
energy,
|
||||||
combat_rating: combat::combat_rating(
|
combat_rating: health.map_or(0.0, |health| {
|
||||||
inventory, health, stats, *body, &msm,
|
combat::combat_rating(inventory, health, stats, *body, &msm)
|
||||||
),
|
}),
|
||||||
});
|
});
|
||||||
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
|
let bubble = if dist_sqr < SPEECH_BUBBLE_RANGE.powi(2) {
|
||||||
speech_bubbles.get(uid)
|
speech_bubbles.get(uid)
|
||||||
@ -1492,7 +1492,8 @@ impl Hud {
|
|||||||
});
|
});
|
||||||
// Divide by 10 to stay in the same dimension as the HP display
|
// Divide by 10 to stay in the same dimension as the HP display
|
||||||
let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs();
|
let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs();
|
||||||
let max_hp_frac = hp_damage.abs() as f32 / health.maximum() as f32;
|
let max_hp_frac =
|
||||||
|
hp_damage.abs() as f32 / health.map_or(1.0, |h| h.maximum() as f32);
|
||||||
let timer = floaters
|
let timer = floaters
|
||||||
.last()
|
.last()
|
||||||
.expect("There must be at least one floater")
|
.expect("There must be at least one floater")
|
||||||
@ -1563,8 +1564,8 @@ impl Hud {
|
|||||||
let sct_bg_id = sct_bg_walker
|
let sct_bg_id = sct_bg_walker
|
||||||
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
|
.next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
|
||||||
// Calculate total change
|
// Calculate total change
|
||||||
let max_hp_frac =
|
let max_hp_frac = floater.hp_change.abs() as f32
|
||||||
floater.hp_change.abs() as f32 / health.maximum() as f32;
|
/ health.map_or(1.0, |h| h.maximum() as f32);
|
||||||
// Increase font size based on fraction of maximum health
|
// Increase font size based on fraction of maximum health
|
||||||
// "flashes" by having a larger size in the first 100ms
|
// "flashes" by having a larger size in the first 100ms
|
||||||
let font_size = 30
|
let font_size = 30
|
||||||
|
@ -57,7 +57,7 @@ widget_ids! {
|
|||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Info<'a> {
|
pub struct Info<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub health: &'a Health,
|
pub health: Option<&'a Health>,
|
||||||
pub buffs: &'a Buffs,
|
pub buffs: &'a Buffs,
|
||||||
pub energy: Option<&'a Energy>,
|
pub energy: Option<&'a Energy>,
|
||||||
pub combat_rating: f32,
|
pub combat_rating: f32,
|
||||||
@ -140,7 +140,7 @@ impl<'a> Ingameable for Overhead<'a> {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
+ if should_show_healthbar(info.health) {
|
+ if info.health.map_or(false, |h| should_show_healthbar(h)) {
|
||||||
5 + if info.energy.is_some() { 1 } else { 0 }
|
5 + if info.energy.is_some() { 1 } else { 0 }
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
@ -176,10 +176,11 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
}) = self.info
|
}) = self.info
|
||||||
{
|
{
|
||||||
// Used to set healthbar colours based on hp_percentage
|
// Used to set healthbar colours based on hp_percentage
|
||||||
let hp_percentage = health.current() as f64 / health.maximum() as f64 * 100.0;
|
let hp_percentage =
|
||||||
|
health.map_or(100.0, |h| h.current() as f64 / h.maximum() as f64 * 100.0);
|
||||||
// Compare levels to decide if a skull is shown
|
// Compare levels to decide if a skull is shown
|
||||||
let health_current = (health.current() / 10) as f64;
|
let health_current = health.map_or(1.0, |h| (h.current() / 10) as f64);
|
||||||
let health_max = (health.maximum() / 10) as f64;
|
let health_max = health.map_or(1.0, |h| (h.maximum() / 10) as f64);
|
||||||
let name_y = if (health_current - health_max).abs() < 1e-6 {
|
let name_y = if (health_current - health_max).abs() < 1e-6 {
|
||||||
MANA_BAR_Y + 20.0
|
MANA_BAR_Y + 20.0
|
||||||
} else {
|
} else {
|
||||||
@ -296,7 +297,8 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.name, ui);
|
.set(state.ids.name, ui);
|
||||||
|
|
||||||
if should_show_healthbar(health) {
|
match health {
|
||||||
|
Some(health) if should_show_healthbar(health) => {
|
||||||
// Show HP Bar
|
// Show HP Bar
|
||||||
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
|
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer
|
||||||
let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
|
let crit_hp_color: Color = Color::Rgba(0.93, 0.59, 0.03, hp_ani);
|
||||||
@ -383,7 +385,8 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
let artifact_diffculty = 122.0;
|
let artifact_diffculty = 122.0;
|
||||||
|
|
||||||
if combat_rating > artifact_diffculty && !self.in_group {
|
if combat_rating > artifact_diffculty && !self.in_group {
|
||||||
let skull_ani = ((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
|
let skull_ani =
|
||||||
|
((self.pulse * 0.7/* speed factor */).cos() * 0.5 + 0.5) * 10.0; //Animation timer
|
||||||
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
|
Image::new(if skull_ani as i32 == 1 && rand::random::<f32>() < 0.9 {
|
||||||
self.imgs.skull_2
|
self.imgs.skull_2
|
||||||
} else {
|
} else {
|
||||||
@ -406,6 +409,8 @@ impl<'a> Widget for Overhead<'a> {
|
|||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.level, ui);
|
.set(state.ids.level, ui);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Speech bubble
|
// Speech bubble
|
||||||
|
Loading…
Reference in New Issue
Block a user