mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'imbris/small-tweaks' into 'master'
Various small tweaks / fixes Closes #1475 See merge request veloren/veloren!3817
This commit is contained in:
commit
61cb0ad39b
@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Lamps, embers and campfires use glowing indices
|
||||
- Non-potion drinks no longer heal as much as potions.
|
||||
- Added SFX to the new sword abilities
|
||||
- Fixed various issues with showing the correct text hint for interactable blocks.
|
||||
- Intert entities like arrows no longer obstruct interacting with nearby entities/blocks.
|
||||
|
||||
## [0.14.0] - 2023-01-07
|
||||
|
||||
|
@ -45,6 +45,8 @@ hud-use = Use
|
||||
hud-unlock-requires = Open with { $item }
|
||||
hud-unlock-consumes = Use { $item } to open
|
||||
hud-mine = Mine
|
||||
hud-mine-needs_pickaxe = Needs Pickaxe
|
||||
hud-mine-needs_unhandled_case = Needs ???
|
||||
hud-talk = Talk
|
||||
hud-trade = Trade
|
||||
hud-mount = Mount
|
||||
|
@ -1805,6 +1805,13 @@ impl Client {
|
||||
.ecs()
|
||||
.fetch::<EventBus<common::event::ServerEvent>>()
|
||||
.recv_all();
|
||||
// TODO: avoid emitting these in the first place OR actually use outcomes
|
||||
// generated locally on the client (if they can be deduplicated from
|
||||
// ones that the server generates or if the client can reliably generate
|
||||
// them (e.g. syncing skipping character states past certain
|
||||
// stages might skip points where outcomes are generated, however we might not
|
||||
// care about this?) and the server doesn't need to send them)
|
||||
let _ = self.state.ecs().fetch::<EventBus<Outcome>>().recv_all();
|
||||
|
||||
// 5) Terrain
|
||||
self.tick_terrain()?;
|
||||
|
@ -45,6 +45,7 @@ pub enum InventoryManip {
|
||||
Pickup(Uid),
|
||||
Collect {
|
||||
sprite_pos: Vec3<i32>,
|
||||
/// If second field is `true`, item will be consumed on collection.
|
||||
required_item: Option<(InvSlotId, bool)>,
|
||||
},
|
||||
Use(Slot),
|
||||
|
@ -462,7 +462,10 @@ impl ItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
// TODO: could this theorectically hold a ref to the actual components and
|
||||
// lazily get their IDs for hash/partialeq/debug/to_owned/etc? (i.e. eliminating
|
||||
// `Vec`s)
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ItemDefinitionId<'a> {
|
||||
Simple(&'a str),
|
||||
Modular {
|
||||
@ -1291,6 +1294,38 @@ pub fn try_all_item_defs() -> Result<Vec<String>, Error> {
|
||||
Ok(defs.ids().map(|id| id.to_string()).collect())
|
||||
}
|
||||
|
||||
impl PartialEq<ItemDefinitionId<'_>> for ItemDefinitionIdOwned {
|
||||
fn eq(&self, other: &ItemDefinitionId<'_>) -> bool {
|
||||
use ItemDefinitionId as DefId;
|
||||
match self {
|
||||
Self::Simple(simple) => {
|
||||
matches!(other, DefId::Simple(other_simple) if simple == other_simple)
|
||||
},
|
||||
Self::Modular {
|
||||
pseudo_base,
|
||||
components,
|
||||
} => matches!(
|
||||
other,
|
||||
DefId::Modular { pseudo_base: other_base, components: other_comps }
|
||||
if pseudo_base == other_base && components == other_comps
|
||||
),
|
||||
Self::Compound {
|
||||
simple_base,
|
||||
components,
|
||||
} => matches!(
|
||||
other,
|
||||
DefId::Compound { simple_base: other_base, components: other_comps }
|
||||
if simple_base == other_base && components == other_comps
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<ItemDefinitionIdOwned> for ItemDefinitionId<'_> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &ItemDefinitionIdOwned) -> bool { other == self }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -400,11 +400,6 @@ impl Inventory {
|
||||
self.slots_with_id()
|
||||
.find(|&(_, it)| {
|
||||
if let Some(it) = it {
|
||||
if it.components().len() == item.components().len() {
|
||||
// TODO: add a ComponentKey struct to compare components, see issue #1226
|
||||
debug_assert!(it.components().is_empty());
|
||||
debug_assert!(item.components().is_empty());
|
||||
}
|
||||
it.item_definition_id() == item.item_definition_id()
|
||||
} else {
|
||||
false
|
||||
@ -413,6 +408,21 @@ impl Inventory {
|
||||
.map(|(slot, _)| slot)
|
||||
}
|
||||
|
||||
pub fn get_slot_of_item_by_def_id(
|
||||
&self,
|
||||
item_def_id: &item::ItemDefinitionIdOwned,
|
||||
) -> Option<InvSlotId> {
|
||||
self.slots_with_id()
|
||||
.find(|&(_, it)| {
|
||||
if let Some(it) = it {
|
||||
it.item_definition_id() == *item_def_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|(slot, _)| slot)
|
||||
}
|
||||
|
||||
/// Get content of a slot
|
||||
pub fn get(&self, inv_slot_id: InvSlotId) -> Option<&Item> {
|
||||
self.slot(inv_slot_id).and_then(Option::as_ref)
|
||||
|
@ -456,6 +456,7 @@ struct EqualitySet {
|
||||
|
||||
impl EqualitySet {
|
||||
fn canonical<'a>(&'a self, item_name: &'a ItemDefinitionIdOwned) -> &'a ItemDefinitionIdOwned {
|
||||
// TODO: use hashbrown Equivalent trait to avoid needing owned item def here
|
||||
let canonical_itemname = self
|
||||
.equivalence_class
|
||||
.get(item_name)
|
||||
@ -996,7 +997,7 @@ impl TradePricing {
|
||||
result
|
||||
}
|
||||
|
||||
fn get_materials_impl(&self, item: &ItemDefinitionId) -> Option<MaterialUse> {
|
||||
fn get_materials_impl(&self, item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
|
||||
self.price_lookup(&item.to_owned()).cloned()
|
||||
}
|
||||
|
||||
@ -1012,7 +1013,7 @@ impl TradePricing {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_materials(item: &ItemDefinitionId) -> Option<MaterialUse> {
|
||||
pub fn get_materials(item: &ItemDefinitionId<'_>) -> Option<MaterialUse> {
|
||||
TRADE_PRICING.get_materials_impl(item)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
use super::utils::*;
|
||||
use crate::{
|
||||
comp::{
|
||||
character_state::OutputEvents,
|
||||
item::{Item, ItemDefinitionIdOwned},
|
||||
character_state::OutputEvents, inventory::slot::InvSlotId, item::ItemDefinitionIdOwned,
|
||||
CharacterState, InventoryManip, StateUpdate,
|
||||
},
|
||||
event::{LocalEvent, ServerEvent},
|
||||
@ -33,8 +32,13 @@ pub struct StaticData {
|
||||
/// Was sneaking
|
||||
pub was_sneak: bool,
|
||||
/// The item required to interact with the sprite, if one was required
|
||||
// If second field is true, item should be consumed on collection
|
||||
pub required_item: Option<(ItemDefinitionIdOwned, bool)>,
|
||||
///
|
||||
/// The second field is the slot that the required item was in when this
|
||||
/// state was created. If it isn't in this slot anymore the interaction will
|
||||
/// fail.
|
||||
///
|
||||
/// If third field is true, item should be consumed on collection
|
||||
pub required_item: Option<(ItemDefinitionIdOwned, InvSlotId, bool)>,
|
||||
/// Miscellaneous information about the ability
|
||||
pub ability_info: AbilityInfo,
|
||||
}
|
||||
@ -97,43 +101,33 @@ impl CharacterBehavior for Data {
|
||||
}
|
||||
} else {
|
||||
// Create inventory manipulation event
|
||||
let required_item =
|
||||
self.static_data
|
||||
.required_item
|
||||
.as_ref()
|
||||
.and_then(|(i, consume)| {
|
||||
Some((
|
||||
Item::new_from_item_definition_id(
|
||||
i.as_ref(),
|
||||
data.ability_map,
|
||||
data.msm,
|
||||
)
|
||||
.ok()?,
|
||||
*consume,
|
||||
))
|
||||
});
|
||||
let has_required_item =
|
||||
required_item.as_ref().map_or(true, |(item, _consume)| {
|
||||
data.inventory.map_or(false, |inv| inv.contains(item))
|
||||
let (has_required_item, inv_slot) = self
|
||||
.static_data
|
||||
.required_item
|
||||
.as_ref()
|
||||
.map_or((true, None), |&(ref item_def_id, slot, consume)| {
|
||||
// Check that required item is still in expected slot
|
||||
let has_item = data
|
||||
.inventory
|
||||
.and_then(|inv| inv.get(slot))
|
||||
.map_or(false, |item| item.item_definition_id() == *item_def_id);
|
||||
|
||||
(has_item, has_item.then_some((slot, consume)))
|
||||
});
|
||||
if has_required_item {
|
||||
let inv_slot = required_item.and_then(|(item, consume)| {
|
||||
Some((
|
||||
data.inventory.and_then(|inv| inv.get_slot_of_item(&item))?,
|
||||
consume,
|
||||
))
|
||||
});
|
||||
let inv_manip = InventoryManip::Collect {
|
||||
sprite_pos: self.static_data.sprite_pos,
|
||||
required_item: inv_slot,
|
||||
};
|
||||
output_events
|
||||
.emit_server(ServerEvent::InventoryManip(data.entity, inv_manip));
|
||||
output_events.emit_local(LocalEvent::CreateOutcome(
|
||||
Outcome::SpriteUnlocked {
|
||||
pos: self.static_data.sprite_pos,
|
||||
},
|
||||
));
|
||||
if matches!(self.static_data.sprite_kind, SpriteInteractKind::Unlock) {
|
||||
output_events.emit_local(LocalEvent::CreateOutcome(
|
||||
Outcome::SpriteUnlocked {
|
||||
pos: self.static_data.sprite_pos,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
// Done
|
||||
end_ability(data, &mut update);
|
||||
|
@ -7,7 +7,7 @@ use crate::{
|
||||
character_state::OutputEvents,
|
||||
controller::InventoryManip,
|
||||
inventory::slot::{ArmorSlot, EquipSlot, Slot},
|
||||
item::{armor::Friction, tool::AbilityContext, Hands, Item, ItemKind, ToolKind},
|
||||
item::{armor::Friction, tool::AbilityContext, Hands, ItemKind, ToolKind},
|
||||
quadruped_low, quadruped_medium, quadruped_small,
|
||||
skills::{Skill, SwimSkill, SKILL_MODIFIERS},
|
||||
theropod, Body, CharacterAbility, CharacterState, Density, InputAttr, InputKind,
|
||||
@ -17,9 +17,9 @@ use crate::{
|
||||
event::{LocalEvent, ServerEvent},
|
||||
outcome::Outcome,
|
||||
states::{behavior::JoinData, utils::CharacterState::Idle, *},
|
||||
terrain::{TerrainChunkSize, UnlockKind},
|
||||
terrain::{TerrainGrid, UnlockKind},
|
||||
util::Dir,
|
||||
vol::{ReadVol, RectVolSize},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use core::hash::BuildHasherDefault;
|
||||
use fxhash::FxHasher64;
|
||||
@ -844,10 +844,7 @@ pub fn handle_manipulate_loadout(
|
||||
if close_to_sprite {
|
||||
// First, get sprite data for position, if there is a sprite
|
||||
use sprite_interact::SpriteInteractKind;
|
||||
let sprite_chunk_pos = sprite_pos
|
||||
.xy()
|
||||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.rem_euclid(sz as i32))
|
||||
.with_z(sprite_pos.z);
|
||||
let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
|
||||
let sprite_cfg = data
|
||||
.terrain
|
||||
.pos_chunk(sprite_pos)
|
||||
@ -918,24 +915,23 @@ pub fn handle_manipulate_loadout(
|
||||
UnlockKind::Requires(item) => Some((item, false)),
|
||||
UnlockKind::Consumes(item) => Some((item, true)),
|
||||
});
|
||||
let has_required_items = required_item
|
||||
.as_ref()
|
||||
.and_then(|(i, _consume)| {
|
||||
Item::new_from_item_definition_id(
|
||||
i.as_ref(),
|
||||
data.ability_map,
|
||||
data.msm,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map_or(true, |i| {
|
||||
data.inventory.map_or(false, |inv| inv.contains(&i))
|
||||
});
|
||||
|
||||
// None: An required items exist but no available
|
||||
// Some(None): No required items
|
||||
// Some(Some(_)): Required items satisfied, contains info about them
|
||||
let has_required_items = match required_item {
|
||||
// Produces `None` if we can't find the item or `Some(Some(_))` if we can
|
||||
Some((item_id, consume)) => data
|
||||
.inventory
|
||||
.and_then(|inv| inv.get_slot_of_item_by_def_id(&item_id))
|
||||
.map(|slot| Some((item_id, slot, consume))),
|
||||
None => Some(None),
|
||||
};
|
||||
|
||||
// If path can be found between entity interacting with sprite and entity, start
|
||||
// interaction with sprite
|
||||
if not_blocked_by_terrain {
|
||||
if has_required_items {
|
||||
if let Some(required_item) = has_required_items {
|
||||
// If the sprite is collectible, enter the sprite interaction character
|
||||
// state TODO: Handle cases for sprite being
|
||||
// interactible, but not collectible (none currently
|
||||
|
@ -221,12 +221,19 @@ impl<V: RectRasterableVol> VolGrid2d<V> {
|
||||
|
||||
pub fn remove(&mut self, key: Vec2<i32>) -> Option<Arc<V>> { self.chunks.remove(&key) }
|
||||
|
||||
/// Converts a chunk key (i.e. coordinates in terms of chunks) into a
|
||||
/// position in the world (aka "wpos").
|
||||
///
|
||||
/// The returned position will be in the corner of the chunk.
|
||||
#[inline(always)]
|
||||
pub fn key_pos(&self, key: Vec2<i32>) -> Vec2<i32> { Self::key_chunk(key) }
|
||||
|
||||
/// Converts a position in the world into a chunk key (i.e. coordinates in
|
||||
/// terms of chunks).
|
||||
#[inline(always)]
|
||||
pub fn pos_key(&self, pos: Vec3<i32>) -> Vec2<i32> { Self::chunk_key(pos) }
|
||||
|
||||
/// Gets the chunk that contains the provided world position.
|
||||
#[inline(always)]
|
||||
pub fn pos_chunk(&self, pos: Vec3<i32>) -> Option<&V> { self.get_key(self.pos_key(pos)) }
|
||||
|
||||
|
@ -68,6 +68,10 @@ impl BlockChange {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the block at given position `pos` has already been modified
|
||||
/// this tick.
|
||||
pub fn can_set_block(&self, pos: Vec3<i32>) -> bool { !self.blocks.contains_key(&pos) }
|
||||
|
||||
pub fn clear(&mut self) { self.blocks.clear(); }
|
||||
}
|
||||
|
||||
@ -462,11 +466,7 @@ impl State {
|
||||
/// Check if the block at given position `pos` has already been modified
|
||||
/// this tick.
|
||||
pub fn can_set_block(&self, pos: Vec3<i32>) -> bool {
|
||||
!self
|
||||
.ecs
|
||||
.read_resource::<BlockChange>()
|
||||
.blocks
|
||||
.contains_key(&pos)
|
||||
self.ecs.read_resource::<BlockChange>().can_set_block(pos)
|
||||
}
|
||||
|
||||
/// Removes every chunk of the terrain.
|
||||
|
@ -1039,13 +1039,13 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, targ
|
||||
use common::terrain::SpriteKind;
|
||||
let pos = pos.map(|e| e.floor() as i32);
|
||||
if let Some(block) = terrain.get(pos).ok().copied().filter(|b| b.is_bonkable()) {
|
||||
if let Some(item) = comp::Item::try_reclaim_from_block(block) {
|
||||
if block_change
|
||||
.try_set(pos, block.with_sprite(SpriteKind::Empty))
|
||||
.is_some()
|
||||
{
|
||||
drop(terrain);
|
||||
drop(block_change);
|
||||
if block_change
|
||||
.try_set(pos, block.with_sprite(SpriteKind::Empty))
|
||||
.is_some()
|
||||
{
|
||||
drop(terrain);
|
||||
drop(block_change);
|
||||
if let Some(item) = comp::Item::try_reclaim_from_block(block) {
|
||||
server
|
||||
.state
|
||||
.create_object(Default::default(), match block.get_sprite() {
|
||||
@ -1064,7 +1064,7 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, targ
|
||||
})
|
||||
.build();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -252,17 +252,21 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
sprite_pos,
|
||||
required_item,
|
||||
} => {
|
||||
let block = state.terrain().get(sprite_pos).ok().copied();
|
||||
let ecs = state.ecs();
|
||||
let terrain = ecs.read_resource::<common::terrain::TerrainGrid>();
|
||||
let mut block_change = ecs.write_resource::<common_state::BlockChange>();
|
||||
|
||||
let block = terrain.get(sprite_pos).ok().copied();
|
||||
let mut drop_item = None;
|
||||
|
||||
if let Some(block) = block {
|
||||
if block.is_collectible() && state.can_set_block(sprite_pos) {
|
||||
if block.is_collectible() && block_change.can_set_block(sprite_pos) {
|
||||
// If an item was required to collect the sprite, consume it now
|
||||
if let Some((inv_slot, true)) = required_item {
|
||||
inventory.take(
|
||||
inv_slot,
|
||||
&state.ecs().read_resource::<AbilityMap>(),
|
||||
&state.ecs().read_resource::<MaterialStatManifest>(),
|
||||
&ecs.read_resource::<AbilityMap>(),
|
||||
&ecs.read_resource::<MaterialStatManifest>(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -270,20 +274,19 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
if let Some(item) = comp::Item::try_reclaim_from_block(block) {
|
||||
// NOTE: We dup the item for message purposes.
|
||||
let item_msg = item.duplicate(
|
||||
&state.ecs().read_resource::<AbilityMap>(),
|
||||
&state.ecs().read_resource::<MaterialStatManifest>(),
|
||||
&ecs.read_resource::<AbilityMap>(),
|
||||
&ecs.read_resource::<MaterialStatManifest>(),
|
||||
);
|
||||
let event = match inventory.push(item) {
|
||||
Ok(_) => {
|
||||
let ecs = state.ecs();
|
||||
if let Some(group_id) = ecs.read_storage::<Group>().get(entity) {
|
||||
announce_loot_to_group(
|
||||
group_id,
|
||||
ecs,
|
||||
entity,
|
||||
item_msg.duplicate(
|
||||
&state.ecs().read_resource::<AbilityMap>(),
|
||||
&state.ecs().read_resource::<MaterialStatManifest>(),
|
||||
&ecs.read_resource::<AbilityMap>(),
|
||||
&ecs.read_resource::<MaterialStatManifest>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -303,15 +306,13 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
)
|
||||
},
|
||||
};
|
||||
state
|
||||
.ecs()
|
||||
.write_storage()
|
||||
ecs.write_storage()
|
||||
.insert(entity, event)
|
||||
.expect("We know entity exists since we got its inventory.");
|
||||
}
|
||||
|
||||
// We made sure earlier the block was not already modified this tick
|
||||
state.set_block(sprite_pos, block.into_vacant());
|
||||
block_change.set(sprite_pos, block.into_vacant());
|
||||
|
||||
// If the block was a keyhole, remove nearby door blocks
|
||||
// TODO: Abstract this code into a generalised way to do block updates?
|
||||
@ -339,11 +340,11 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
pending.remove(&pos);
|
||||
if !destroyed.contains(&pos)
|
||||
&& matches!(
|
||||
state.terrain().get(pos).ok().and_then(|b| b.get_sprite()),
|
||||
terrain.get(pos).ok().and_then(|b| b.get_sprite()),
|
||||
Some(SpriteKind::KeyDoor)
|
||||
)
|
||||
{
|
||||
state.set_block(pos, Block::empty());
|
||||
block_change.try_set(pos, Block::empty());
|
||||
destroyed.insert(pos);
|
||||
|
||||
pending.extend(dirs.into_iter().map(|dir| pos + dir));
|
||||
@ -362,6 +363,8 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
}
|
||||
}
|
||||
drop(inventories);
|
||||
drop(terrain);
|
||||
drop(block_change);
|
||||
if let Some(item) = drop_item {
|
||||
state
|
||||
.create_item_drop(Default::default(), item)
|
||||
@ -370,7 +373,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
sprite_pos.x as f32,
|
||||
sprite_pos.y as f32,
|
||||
sprite_pos.z as f32,
|
||||
) + Vec3::unit_z(),
|
||||
) + Vec3::one().with_z(0.0) * 0.5,
|
||||
))
|
||||
.with(comp::Vel(Vec3::zero()))
|
||||
.build();
|
||||
|
@ -65,12 +65,9 @@ use crate::{
|
||||
game_input::GameInput,
|
||||
hud::{img_ids::ImgsRot, prompt_dialog::DialogOutcomeEvent},
|
||||
render::UiDrawer,
|
||||
scene::{
|
||||
camera::{self, Camera},
|
||||
terrain::Interaction,
|
||||
},
|
||||
scene::camera::{self, Camera},
|
||||
session::{
|
||||
interactable::Interactable,
|
||||
interactable::{BlockInteraction, Interactable},
|
||||
settings_change::{
|
||||
Audio, Chat as ChatChange, Interface as InterfaceChange, SettingsChange,
|
||||
},
|
||||
@ -674,6 +671,7 @@ pub struct DebugInfo {
|
||||
|
||||
pub struct HudInfo {
|
||||
pub is_aiming: bool,
|
||||
pub is_mining: bool,
|
||||
pub is_first_person: bool,
|
||||
pub viewpoint_entity: specs::Entity,
|
||||
pub mutable_viewpoint: bool,
|
||||
@ -2021,7 +2019,10 @@ impl Hud {
|
||||
pickup_failed_pulse: self.failed_entity_pickups.get(&entity).cloned(),
|
||||
},
|
||||
&self.fonts,
|
||||
vec![(GameInput::Interact, i18n.get_msg("hud-pick_up").to_string())],
|
||||
vec![(
|
||||
Some(GameInput::Interact),
|
||||
i18n.get_msg("hud-pick_up").to_string(),
|
||||
)],
|
||||
)
|
||||
.set(overitem_id, ui_widgets);
|
||||
}
|
||||
@ -2041,31 +2042,58 @@ impl Hud {
|
||||
let over_pos = pos + Vec3::unit_z() * 0.7;
|
||||
|
||||
let interaction_text = || match interaction {
|
||||
Interaction::Collect => {
|
||||
vec![(GameInput::Interact, i18n.get_msg("hud-collect").to_string())]
|
||||
BlockInteraction::Collect => {
|
||||
vec![(
|
||||
Some(GameInput::Interact),
|
||||
i18n.get_msg("hud-collect").to_string(),
|
||||
)]
|
||||
},
|
||||
Interaction::Craft(_) => {
|
||||
vec![(GameInput::Interact, i18n.get_msg("hud-use").to_string())]
|
||||
BlockInteraction::Craft(_) => {
|
||||
vec![(
|
||||
Some(GameInput::Interact),
|
||||
i18n.get_msg("hud-use").to_string(),
|
||||
)]
|
||||
},
|
||||
Interaction::Unlock(kind) => vec![(GameInput::Interact, match kind {
|
||||
UnlockKind::Free => i18n.get_msg("hud-open").to_string(),
|
||||
UnlockKind::Requires(item) => i18n
|
||||
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
|
||||
"item" => item.as_ref().itemdef_id()
|
||||
.map(|id| Item::new_from_asset_expect(id).describe())
|
||||
.unwrap_or_else(|| "modular item".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
UnlockKind::Consumes(item) => i18n
|
||||
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
|
||||
"item" => item.as_ref().itemdef_id()
|
||||
.map(|id| Item::new_from_asset_expect(id).describe())
|
||||
.unwrap_or_else(|| "modular item".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
})],
|
||||
Interaction::Mine => {
|
||||
vec![(GameInput::Primary, i18n.get_msg("hud-mine").to_string())]
|
||||
BlockInteraction::Unlock(kind) => {
|
||||
vec![(Some(GameInput::Interact), match kind {
|
||||
UnlockKind::Free => i18n.get_msg("hud-open").to_string(),
|
||||
UnlockKind::Requires(item) => i18n
|
||||
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
|
||||
"item" => item.as_ref().itemdef_id()
|
||||
.map(|id| Item::new_from_asset_expect(id).describe())
|
||||
.unwrap_or_else(|| "modular item".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
UnlockKind::Consumes(item) => i18n
|
||||
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
|
||||
"item" => item.as_ref().itemdef_id()
|
||||
.map(|id| Item::new_from_asset_expect(id).describe())
|
||||
.unwrap_or_else(|| "modular item".to_string()),
|
||||
})
|
||||
.to_string(),
|
||||
})]
|
||||
},
|
||||
BlockInteraction::Mine(mine_tool) => {
|
||||
if info.is_mining {
|
||||
vec![(
|
||||
Some(GameInput::Primary),
|
||||
i18n.get_msg("hud-mine").to_string(),
|
||||
)]
|
||||
} else {
|
||||
match mine_tool {
|
||||
ToolKind::Pick => {
|
||||
vec![(None, i18n.get_msg("hud-mine-needs_pickaxe").to_string())]
|
||||
},
|
||||
// TODO: The required tool for mining something may not always be a
|
||||
// pickaxe!
|
||||
_ => {
|
||||
vec![(
|
||||
None,
|
||||
i18n.get_msg("hud-mine-needs_unhandled_case").to_string(),
|
||||
)]
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@ -2085,7 +2113,10 @@ impl Hud {
|
||||
overitem_properties,
|
||||
self.pulse,
|
||||
&global_state.window.key_layout,
|
||||
vec![(GameInput::Interact, i18n.get_msg("hud-open").to_string())],
|
||||
vec![(
|
||||
Some(GameInput::Interact),
|
||||
i18n.get_msg("hud-open").to_string(),
|
||||
)],
|
||||
)
|
||||
.x_y(0.0, 100.0)
|
||||
.position_ingame(over_pos)
|
||||
@ -2154,7 +2185,10 @@ impl Hud {
|
||||
overitem_properties,
|
||||
self.pulse,
|
||||
&global_state.window.key_layout,
|
||||
vec![(GameInput::Interact, i18n.get_msg("hud-sit").to_string())],
|
||||
vec![(
|
||||
Some(GameInput::Interact),
|
||||
i18n.get_msg("hud-sit").to_string(),
|
||||
)],
|
||||
)
|
||||
.x_y(0.0, 100.0)
|
||||
.position_ingame(over_pos)
|
||||
|
@ -46,7 +46,8 @@ pub struct Overitem<'a> {
|
||||
properties: OveritemProperties,
|
||||
pulse: f32,
|
||||
key_layout: &'a Option<KeyLayout>,
|
||||
interaction_options: Vec<(GameInput, String)>,
|
||||
// GameInput optional so we can just show stuff like "needs pickaxe"
|
||||
interaction_options: Vec<(Option<GameInput>, String)>,
|
||||
}
|
||||
|
||||
impl<'a> Overitem<'a> {
|
||||
@ -60,7 +61,7 @@ impl<'a> Overitem<'a> {
|
||||
properties: OveritemProperties,
|
||||
pulse: f32,
|
||||
key_layout: &'a Option<KeyLayout>,
|
||||
interaction_options: Vec<(GameInput, String)>,
|
||||
interaction_options: Vec<(Option<GameInput>, String)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
@ -177,19 +178,20 @@ impl<'a> Widget for Overitem<'a> {
|
||||
.interaction_options
|
||||
.iter()
|
||||
.filter_map(|(input, action)| {
|
||||
Some((
|
||||
self.controls
|
||||
.get_binding(*input)
|
||||
.filter(|_| self.properties.active)?,
|
||||
action,
|
||||
))
|
||||
let binding = if let Some(input) = input {
|
||||
Some(self.controls.get_binding(*input)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some((binding, action))
|
||||
})
|
||||
.map(|(input, action)| {
|
||||
format!(
|
||||
"{} {}",
|
||||
input.display_string(self.key_layout).as_str(),
|
||||
action
|
||||
)
|
||||
if let Some(input) = input {
|
||||
let input = input.display_string(self.key_layout);
|
||||
format!("{} {action}", input.as_str())
|
||||
} else {
|
||||
action.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
@ -1,16 +1,16 @@
|
||||
use crate::hud::CraftingTab;
|
||||
use common::terrain::{BlockKind, SpriteKind, TerrainChunk, UnlockKind};
|
||||
use common::terrain::{BlockKind, SpriteKind, TerrainChunk};
|
||||
use common_base::span;
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Interaction {
|
||||
/// This covers mining, unlocking, and regular collectable things (e.g.
|
||||
/// twigs).
|
||||
Collect,
|
||||
Unlock(UnlockKind),
|
||||
Craft(CraftingTab),
|
||||
Mine,
|
||||
}
|
||||
|
||||
pub enum FireplaceType {
|
||||
@ -169,17 +169,7 @@ impl BlocksOfInterest {
|
||||
},
|
||||
}
|
||||
if block.collectible_id().is_some() {
|
||||
interactables.push((
|
||||
pos,
|
||||
block
|
||||
.get_sprite()
|
||||
.map(|s| {
|
||||
Interaction::Unlock(
|
||||
s.unlock_condition(chunk.meta().sprite_cfg_at(pos).cloned()),
|
||||
)
|
||||
})
|
||||
.unwrap_or(Interaction::Collect),
|
||||
));
|
||||
interactables.push((pos, Interaction::Collect));
|
||||
}
|
||||
if let Some(glow) = block.get_glow() {
|
||||
// Currently, we count filled blocks as 'minor' lights, and sprites as
|
||||
|
@ -9,22 +9,34 @@ use super::{
|
||||
use client::Client;
|
||||
use common::{
|
||||
comp,
|
||||
comp::tool::ToolKind,
|
||||
consts::MAX_PICKUP_RANGE,
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
terrain::Block,
|
||||
terrain::{Block, TerrainGrid, UnlockKind},
|
||||
util::find_dist::{Cube, Cylinder, FindDist},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_base::span;
|
||||
|
||||
use crate::scene::{terrain::Interaction, Scene};
|
||||
use crate::{
|
||||
hud::CraftingTab,
|
||||
scene::{terrain::Interaction, Scene},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BlockInteraction {
|
||||
Collect,
|
||||
Unlock(UnlockKind),
|
||||
Craft(CraftingTab),
|
||||
// TODO: mining blocks don't use the interaction key, so it might not be the best abstraction
|
||||
// to have them here, will see how things turn out
|
||||
Mine(ToolKind),
|
||||
}
|
||||
|
||||
// TODO: extract mining blocks (the None case in the Block variant) from this
|
||||
// enum since they don't use the interaction key
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Interactable {
|
||||
Block(Block, Vec3<i32>, Interaction),
|
||||
Block(Block, Vec3<i32>, BlockInteraction),
|
||||
Entity(specs::Entity),
|
||||
}
|
||||
|
||||
@ -35,6 +47,44 @@ impl Interactable {
|
||||
Self::Block(_, _, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_block_pos(
|
||||
terrain: &TerrainGrid,
|
||||
pos: Vec3<i32>,
|
||||
interaction: Interaction,
|
||||
) -> Option<Self> {
|
||||
let Ok(&block) = terrain.get(pos) else { return None };
|
||||
let block_interaction = match interaction {
|
||||
Interaction::Collect => {
|
||||
// Check if this is an unlockable sprite
|
||||
let unlock = block.get_sprite().and_then(|sprite| {
|
||||
let Some(chunk) = terrain.pos_chunk(pos) else { return None };
|
||||
let sprite_chunk_pos = TerrainGrid::chunk_offs(pos);
|
||||
let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
|
||||
let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned());
|
||||
// HACK: No other way to distinguish between things that should be unlockable
|
||||
// and regular sprites with the current unlock_condition method so we hack
|
||||
// around that by saying that it is a regular collectible sprite if
|
||||
// `unlock_condition` returns UnlockKind::Free and the cfg was `None`.
|
||||
if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) {
|
||||
Some(unlock_condition)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(unlock) = unlock {
|
||||
BlockInteraction::Unlock(unlock)
|
||||
} else if let Some(mine_tool) = block.mine_tool() {
|
||||
BlockInteraction::Mine(mine_tool)
|
||||
} else {
|
||||
BlockInteraction::Collect
|
||||
}
|
||||
},
|
||||
Interaction::Craft(tab) => BlockInteraction::Craft(tab),
|
||||
};
|
||||
Some(Self::Block(block, pos, block_interaction))
|
||||
}
|
||||
}
|
||||
|
||||
/// Select interactable to highlight, display interaction text for, and to
|
||||
@ -62,14 +112,7 @@ pub(super) fn select_interactable(
|
||||
collect_target.map(|t| t.distance),
|
||||
]);
|
||||
|
||||
fn get_block<T>(client: &Client, target: Target<T>) -> Option<Block> {
|
||||
client
|
||||
.state()
|
||||
.terrain()
|
||||
.get(target.position_int())
|
||||
.ok()
|
||||
.copied()
|
||||
}
|
||||
let terrain = client.state().terrain();
|
||||
|
||||
if let Some(interactable) = entity_target
|
||||
.and_then(|t| {
|
||||
@ -83,8 +126,9 @@ pub(super) fn select_interactable(
|
||||
.or_else(|| {
|
||||
collect_target.and_then(|t| {
|
||||
if Some(t.distance) == nearest_dist {
|
||||
get_block(client, t)
|
||||
.map(|b| Interactable::Block(b, t.position_int(), Interaction::Collect))
|
||||
terrain.get(t.position_int()).ok().map(|&b| {
|
||||
Interactable::Block(b, t.position_int(), BlockInteraction::Collect)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -93,14 +137,18 @@ pub(super) fn select_interactable(
|
||||
.or_else(|| {
|
||||
mine_target.and_then(|t| {
|
||||
if Some(t.distance) == nearest_dist {
|
||||
get_block(client, t).and_then(|b| {
|
||||
terrain.get(t.position_int()).ok().and_then(|&b| {
|
||||
// Handling edge detection. sometimes the casting (in Target mod) returns a
|
||||
// position which is actually empty, which we do not want labeled as an
|
||||
// interactable. We are only returning the mineable air
|
||||
// elements (e.g. minerals). The mineable weakrock are used
|
||||
// in the terrain selected_pos, but is not an interactable.
|
||||
if b.mine_tool().is_some() && b.is_air() {
|
||||
Some(Interactable::Block(b, t.position_int(), Interaction::Mine))
|
||||
if let Some(mine_tool) = b.mine_tool() && b.is_air() {
|
||||
Some(Interactable::Block(
|
||||
b,
|
||||
t.position_int(),
|
||||
BlockInteraction::Mine(mine_tool),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -124,6 +172,9 @@ pub(super) fn select_interactable(
|
||||
let colliders = ecs.read_storage::<comp::Collider>();
|
||||
let char_states = ecs.read_storage::<comp::CharacterState>();
|
||||
let is_mount = ecs.read_storage::<Is<Mount>>();
|
||||
let bodies = ecs.read_storage::<comp::Body>();
|
||||
let items = ecs.read_storage::<comp::Item>();
|
||||
let stats = ecs.read_storage::<comp::Stats>();
|
||||
|
||||
let player_cylinder = Cylinder::from_components(
|
||||
player_pos,
|
||||
@ -135,20 +186,39 @@ pub(super) fn select_interactable(
|
||||
let closest_interactable_entity = (
|
||||
&ecs.entities(),
|
||||
&positions,
|
||||
&bodies,
|
||||
scales.maybe(),
|
||||
colliders.maybe(),
|
||||
char_states.maybe(),
|
||||
!&is_mount,
|
||||
(stats.mask() | items.mask()).maybe(),
|
||||
)
|
||||
.join()
|
||||
.filter(|(e, _, _, _, _, _)| *e != player_entity)
|
||||
.map(|(e, p, s, c, cs, _)| {
|
||||
.filter(|&(e, _, _, _, _, _, _, _)| e != player_entity) // skip the player's entity
|
||||
.filter_map(|(e, p, b, s, c, cs, _, has_stats_or_item)| {
|
||||
// Note, if this becomes expensive to compute do it after the distance check!
|
||||
//
|
||||
// The entities that can be interacted with:
|
||||
// * Sitting at campfires (Body::is_campfire)
|
||||
// * Talking/trading with npcs (note hud code uses Alignment but I can't bring
|
||||
// myself to write more code on that depends on having this on the client so
|
||||
// we just check for presence of Stats component for now, it is okay to have
|
||||
// some false positives here as long as it doesn't frequently prevent us from
|
||||
// interacting with actual interactable entities that are closer by)
|
||||
// * Dropped items that can be picked up (Item component)
|
||||
let is_interactable = b.is_campfire() || has_stats_or_item.is_some();
|
||||
if !is_interactable {
|
||||
return None;
|
||||
};
|
||||
|
||||
let cylinder = Cylinder::from_components(p.0, s.copied(), c, cs);
|
||||
(e, cylinder)
|
||||
// Roughly filter out entities farther than interaction distance
|
||||
if player_cylinder.approx_in_range(cylinder, MAX_PICKUP_RANGE) {
|
||||
Some((e, player_cylinder.min_distance(cylinder)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
// Roughly filter out entities farther than interaction distance
|
||||
.filter(|(_, cylinder)| player_cylinder.approx_in_range(*cylinder, MAX_PICKUP_RANGE))
|
||||
.map(|(e, cylinder)| (e, player_cylinder.min_distance(cylinder)))
|
||||
.min_by_key(|(_, dist)| OrderedFloat(*dist));
|
||||
|
||||
// Only search as far as closest interactable entity
|
||||
@ -156,7 +226,7 @@ pub(super) fn select_interactable(
|
||||
let player_chunk = player_pos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
|
||||
(e.floor() as i32).div_euclid(sz as i32)
|
||||
});
|
||||
let terrain = scene.terrain();
|
||||
let scene_terrain = scene.terrain();
|
||||
|
||||
// Find closest interactable block
|
||||
// TODO: consider doing this one first?
|
||||
@ -168,7 +238,7 @@ pub(super) fn select_interactable(
|
||||
let chunk_pos = player_chunk + offset;
|
||||
let chunk_voxel_pos =
|
||||
Vec3::<i32>::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
|
||||
terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos))
|
||||
scene_terrain.get(chunk_pos).map(|data| (data, chunk_voxel_pos))
|
||||
})
|
||||
// TODO: maybe we could make this more efficient by putting the
|
||||
// interactables is some sort of spatial structure
|
||||
@ -180,10 +250,10 @@ pub(super) fn select_interactable(
|
||||
.map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
|
||||
})
|
||||
.map(|(block_pos, interaction)| (
|
||||
block_pos,
|
||||
block_pos.map(|e| e as f32 + 0.5)
|
||||
.distance_squared(player_pos),
|
||||
interaction,
|
||||
block_pos,
|
||||
block_pos.map(|e| e as f32 + 0.5)
|
||||
.distance_squared(player_pos),
|
||||
interaction,
|
||||
))
|
||||
.min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr))
|
||||
.map(|(block_pos, _, interaction)| (block_pos, interaction));
|
||||
@ -197,13 +267,7 @@ pub(super) fn select_interactable(
|
||||
}) < search_dist
|
||||
})
|
||||
.and_then(|(block_pos, interaction)| {
|
||||
client
|
||||
.state()
|
||||
.terrain()
|
||||
.get(block_pos)
|
||||
.ok()
|
||||
.copied()
|
||||
.map(|b| Interactable::Block(b, block_pos, interaction.clone()))
|
||||
Interactable::from_block_pos(&terrain, block_pos, *interaction)
|
||||
})
|
||||
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
|
||||
}
|
||||
|
@ -49,13 +49,13 @@ use crate::{
|
||||
key_state::KeyState,
|
||||
menu::char_selection::CharSelectionState,
|
||||
render::{Drawer, GlobalsBindGroup},
|
||||
scene::{camera, terrain::Interaction, CameraMode, DebugShapeId, Scene, SceneData},
|
||||
scene::{camera, CameraMode, DebugShapeId, Scene, SceneData},
|
||||
settings::Settings,
|
||||
window::{AnalogGameInput, Event},
|
||||
Direction, GlobalState, PlayState, PlayStateResult,
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use interactable::{select_interactable, Interactable};
|
||||
use interactable::{select_interactable, BlockInteraction, Interactable};
|
||||
use settings_change::Language::ChangeLanguage;
|
||||
use target::targets_under_cursor;
|
||||
#[cfg(feature = "egui-ui")]
|
||||
@ -930,19 +930,19 @@ impl PlayState for SessionState {
|
||||
match interactable {
|
||||
Interactable::Block(block, pos, interaction) => {
|
||||
match interaction {
|
||||
Interaction::Collect
|
||||
| Interaction::Unlock(_) => {
|
||||
BlockInteraction::Collect
|
||||
| BlockInteraction::Unlock(_) => {
|
||||
if block.is_collectible() {
|
||||
client.collect_block(*pos);
|
||||
}
|
||||
},
|
||||
Interaction::Craft(tab) => {
|
||||
BlockInteraction::Craft(tab) => {
|
||||
self.hud.show.open_crafting_tab(
|
||||
*tab,
|
||||
block.get_sprite().map(|s| (*pos, s)),
|
||||
)
|
||||
},
|
||||
Interaction::Mine => {},
|
||||
BlockInteraction::Mine(_) => {},
|
||||
}
|
||||
},
|
||||
Interactable::Entity(entity) => {
|
||||
@ -1366,6 +1366,7 @@ impl PlayState for SessionState {
|
||||
global_state.clock.get_stable_dt(),
|
||||
HudInfo {
|
||||
is_aiming,
|
||||
is_mining,
|
||||
is_first_person: matches!(
|
||||
self.scene.camera().get_mode(),
|
||||
camera::CameraMode::FirstPerson
|
||||
|
@ -100,8 +100,7 @@ pub(super) fn targets_under_cursor(
|
||||
}
|
||||
};
|
||||
|
||||
let (collect_pos, _, collect_cam_ray) =
|
||||
find_pos(|b: Block| matches!(b.collectible_id(), Some(Some(_))));
|
||||
let (collect_pos, _, collect_cam_ray) = find_pos(|b: Block| b.is_collectible());
|
||||
let (mine_pos, _, mine_cam_ray) = is_mining
|
||||
.then(|| find_pos(|b: Block| b.mine_tool().is_some()))
|
||||
.unwrap_or((None, None, None));
|
||||
|
@ -271,19 +271,24 @@ pub fn apply_trees_to(
|
||||
})
|
||||
.sum(),
|
||||
) + Vec3::unit_z() * (wpos.z - tree.pos.z);
|
||||
let sblock;
|
||||
block_from_structure(
|
||||
info.index(),
|
||||
&if let Some(block) = match &tree.model {
|
||||
TreeModel::Structure(s) => s.get(model_pos).ok().cloned(),
|
||||
if let Some(block) = match &tree.model {
|
||||
TreeModel::Structure(s) => s.get(model_pos).ok(),
|
||||
TreeModel::Procedural(t, leaf_block) => Some(
|
||||
match t.is_branch_or_leaves_at(model_pos.map(|e| e as f32 + 0.5)) {
|
||||
(_, _, true, _) => {
|
||||
StructureBlock::Filled(BlockKind::Wood, Rgb::new(150, 98, 41))
|
||||
sblock = StructureBlock::Filled(
|
||||
BlockKind::Wood,
|
||||
Rgb::new(150, 98, 41),
|
||||
);
|
||||
&sblock
|
||||
},
|
||||
(_, _, _, true) => StructureBlock::None,
|
||||
(true, _, _, _) => t.config.trunk_block.clone(),
|
||||
(_, true, _, _) => leaf_block.clone(),
|
||||
_ => StructureBlock::None,
|
||||
(_, _, _, true) => &StructureBlock::None,
|
||||
(true, _, _, _) => &t.config.trunk_block,
|
||||
(_, true, _, _) => leaf_block,
|
||||
_ => &StructureBlock::None,
|
||||
},
|
||||
),
|
||||
} {
|
||||
|
Loading…
Reference in New Issue
Block a user