mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Introduced loot ownership rules to combat loot stealing by players
* Added `LootOwner` component used to indicate that an `ItemDrop` entity is owned by another entity * A loot winner is now calculated after EXP allocation using the EXP per entity for weighted chance distribution * Used existing Inventory Full overitem text to show "Owned by {player} for {seconds}secs" when a pickup fails due to a loot ownership check * Updated agent code to take into account loot ownership when searching for `ItemDrop` targets to pick up * Added `loot` ECS system to clear expired loot ownerships
This commit is contained in:
parent
bba81c65d9
commit
34f580dfaa
@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Modular weapons
|
||||
- Added Thai translation
|
||||
- Skiing and ice skating
|
||||
- Added loot ownership for NPC drops
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -10,6 +10,8 @@
|
||||
"hud.waypoint_saved": "Waypoint Saved",
|
||||
"hud.sp_arrow_txt": "SP",
|
||||
"hud.inventory_full": "Inventory Full",
|
||||
"hud.someone_else": "someone else",
|
||||
"hud.owned_by_for_secs": "Owned by {name} for {secs} secs",
|
||||
|
||||
"hud.press_key_to_show_keybindings_fmt": "[{key}] Keybindings",
|
||||
"hud.press_key_to_toggle_lantern_fmt": "[{key}] Lantern",
|
||||
|
@ -2133,8 +2133,8 @@ impl Client {
|
||||
},
|
||||
ServerGeneral::InventoryUpdate(inventory, event) => {
|
||||
match event {
|
||||
InventoryUpdateEvent::BlockCollectFailed(_) => {},
|
||||
InventoryUpdateEvent::EntityCollectFailed(_) => {},
|
||||
InventoryUpdateEvent::BlockCollectFailed { .. } => {},
|
||||
InventoryUpdateEvent::EntityCollectFailed { .. } => {},
|
||||
_ => {
|
||||
// Push the updated inventory component to the client
|
||||
// FIXME: Figure out whether this error can happen under normal gameplay,
|
||||
|
@ -74,7 +74,7 @@ impl WorldSyncExt for specs::World {
|
||||
self.read_storage::<Uid>().get(entity).copied()
|
||||
}
|
||||
|
||||
/// Get the UID of an entity
|
||||
/// Get an entity from a UID
|
||||
fn entity_from_uid(&self, uid: u64) -> Option<specs::Entity> {
|
||||
self.read_resource::<UidAllocator>()
|
||||
.retrieve_entity_internal(uid)
|
||||
|
@ -62,6 +62,7 @@ macro_rules! synced_components {
|
||||
combo: Combo,
|
||||
active_abilities: ActiveAbilities,
|
||||
can_build: CanBuild,
|
||||
loot_owner: LootOwner,
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -234,3 +235,7 @@ impl NetSync for ActiveAbilities {
|
||||
impl NetSync for CanBuild {
|
||||
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
|
||||
}
|
||||
|
||||
impl NetSync for LootOwner {
|
||||
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
|
||||
}
|
||||
|
@ -803,6 +803,12 @@ impl Component for Inventory {
|
||||
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum CollectFailedReason {
|
||||
InventoryFull,
|
||||
LootOwned { owner_uid: Uid, expiry_secs: u64 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum InventoryUpdateEvent {
|
||||
Init,
|
||||
@ -813,8 +819,14 @@ pub enum InventoryUpdateEvent {
|
||||
Swapped,
|
||||
Dropped,
|
||||
Collected(Item),
|
||||
BlockCollectFailed(Vec3<i32>),
|
||||
EntityCollectFailed(Uid),
|
||||
BlockCollectFailed {
|
||||
pos: Vec3<i32>,
|
||||
reason: CollectFailedReason,
|
||||
},
|
||||
EntityCollectFailed {
|
||||
entity: Uid,
|
||||
reason: CollectFailedReason,
|
||||
},
|
||||
Possession,
|
||||
Debug,
|
||||
Craft,
|
||||
|
63
common/src/comp/loot_owner.rs
Normal file
63
common/src/comp/loot_owner.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use crate::{
|
||||
comp::{Alignment, Body, Player},
|
||||
uid::Uid,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specs::{Component, DerefFlaggedStorage};
|
||||
use specs_idvs::IdvStorage;
|
||||
use std::{
|
||||
ops::Add,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LootOwner {
|
||||
// TODO: Fix this if expiry is needed client-side, Instant is not serializable
|
||||
#[serde(skip, default = "Instant::now")]
|
||||
expiry: Instant,
|
||||
owner_uid: Uid,
|
||||
}
|
||||
|
||||
// Loot becomes free-for-all after the initial ownership period
|
||||
const OWNERSHIP_SECS: u64 = 45;
|
||||
|
||||
impl LootOwner {
|
||||
pub fn new(uid: Uid) -> Self {
|
||||
Self {
|
||||
expiry: Instant::now().add(Duration::from_secs(OWNERSHIP_SECS)),
|
||||
owner_uid: uid,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> Uid { self.owner_uid }
|
||||
|
||||
pub fn time_until_expiration(&self) -> Duration { self.expiry - Instant::now() }
|
||||
|
||||
pub fn expired(&self) -> bool { self.expiry <= Instant::now() }
|
||||
|
||||
pub fn default_instant() -> Instant { Instant::now() }
|
||||
|
||||
pub fn can_pickup(
|
||||
&self,
|
||||
uid: Uid,
|
||||
alignment: Option<&Alignment>,
|
||||
body: Option<&Body>,
|
||||
player: Option<&Player>,
|
||||
) -> bool {
|
||||
let is_owned = matches!(alignment, Some(Alignment::Owned(_)));
|
||||
let is_player = player.is_some();
|
||||
let is_pet = is_owned && !is_player;
|
||||
|
||||
let owns_loot = self.uid().0 == uid.0;
|
||||
let is_humanoid = matches!(body, Some(Body::Humanoid(_)));
|
||||
|
||||
// Pet's can't pick up owned loot
|
||||
// Humanoids must own the loot
|
||||
// Non-humanoids ignore loot ownership
|
||||
!is_pet && (owns_loot || !is_humanoid)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for LootOwner {
|
||||
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
|
||||
}
|
@ -29,6 +29,7 @@ pub mod inventory;
|
||||
pub mod invite;
|
||||
#[cfg(not(target_arch = "wasm32"))] mod last;
|
||||
#[cfg(not(target_arch = "wasm32"))] mod location;
|
||||
pub mod loot_owner;
|
||||
#[cfg(not(target_arch = "wasm32"))] pub mod melee;
|
||||
#[cfg(not(target_arch = "wasm32"))] mod misc;
|
||||
#[cfg(not(target_arch = "wasm32"))] pub mod ori;
|
||||
@ -87,10 +88,11 @@ pub use self::{
|
||||
tool::{self, AbilityItem},
|
||||
Item, ItemConfig, ItemDrop,
|
||||
},
|
||||
slot, Inventory, InventoryUpdate, InventoryUpdateEvent,
|
||||
slot, CollectFailedReason, Inventory, InventoryUpdate, InventoryUpdateEvent,
|
||||
},
|
||||
last::Last,
|
||||
location::{MapMarker, MapMarkerChange, MapMarkerUpdate, Waypoint, WaypointArea},
|
||||
loot_owner::LootOwner,
|
||||
melee::{Melee, MeleeConstructor},
|
||||
misc::Object,
|
||||
ori::Ori,
|
||||
|
@ -157,6 +157,7 @@ impl State {
|
||||
ecs.register::<comp::ShockwaveHitEntities>();
|
||||
ecs.register::<comp::BeamSegment>();
|
||||
ecs.register::<comp::Alignment>();
|
||||
ecs.register::<comp::LootOwner>();
|
||||
|
||||
// Register components send from clients -> server
|
||||
ecs.register::<comp::Controller>();
|
||||
|
@ -500,13 +500,12 @@ fn handle_drop_all(
|
||||
|
||||
server
|
||||
.state
|
||||
.create_item_drop(Default::default(), &item)
|
||||
.create_item_drop(Default::default(), item)
|
||||
.with(comp::Pos(Vec3::new(
|
||||
pos.0.x + rng.gen_range(5.0..10.0),
|
||||
pos.0.y + rng.gen_range(5.0..10.0),
|
||||
pos.0.z + 5.0,
|
||||
)))
|
||||
.with(item)
|
||||
.with(comp::Vel(vel))
|
||||
.build();
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use crate::{
|
||||
comp::{
|
||||
ability,
|
||||
agent::{Agent, AgentEvent, Sound, SoundKind},
|
||||
loot_owner::LootOwner,
|
||||
skillset::SkillGroupKind,
|
||||
BuffKind, BuffSource, PhysicsState,
|
||||
},
|
||||
@ -34,7 +35,8 @@ use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
|
||||
use common_state::BlockChange;
|
||||
use comp::chat::GenericChatMsg;
|
||||
use hashbrown::HashSet;
|
||||
use rand::Rng;
|
||||
use rand::{distributions::WeightedIndex, Rng};
|
||||
use rand_distr::Distribution;
|
||||
use specs::{
|
||||
join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
|
||||
};
|
||||
@ -203,6 +205,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
}
|
||||
}
|
||||
|
||||
let mut exp_awards = Vec::<(Entity, f32)>::new();
|
||||
// Award EXP to damage contributors
|
||||
//
|
||||
// NOTE: Debug logging is disabled by default for this module - to enable it add
|
||||
@ -313,7 +316,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
// Iterate through all contributors of damage for the killed entity, calculating
|
||||
// how much EXP each contributor should be awarded based on their
|
||||
// percentage of damage contribution
|
||||
damage_contributors.iter().filter_map(|(damage_contributor, (_, damage_percent))| {
|
||||
exp_awards = damage_contributors.iter().filter_map(|(damage_contributor, (_, damage_percent))| {
|
||||
let contributor_exp = exp_reward * damage_percent;
|
||||
match damage_contributor {
|
||||
DamageContrib::Solo(attacker) => {
|
||||
@ -365,16 +368,23 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
None
|
||||
}
|
||||
}
|
||||
}).flatten().for_each(|(attacker, exp_reward)| {
|
||||
}).flatten().collect::<Vec<(Entity, f32)>>();
|
||||
|
||||
exp_awards.iter().for_each(|(attacker, exp_reward)| {
|
||||
// Process the calculated EXP rewards
|
||||
if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory), Some(pos)) = (
|
||||
skill_sets.get_mut(attacker),
|
||||
uids.get(attacker),
|
||||
inventories.get(attacker),
|
||||
positions.get(attacker),
|
||||
if let (
|
||||
Some(mut attacker_skill_set),
|
||||
Some(attacker_uid),
|
||||
Some(attacker_inventory),
|
||||
Some(pos),
|
||||
) = (
|
||||
skill_sets.get_mut(*attacker),
|
||||
uids.get(*attacker),
|
||||
inventories.get(*attacker),
|
||||
positions.get(*attacker),
|
||||
) {
|
||||
handle_exp_gain(
|
||||
exp_reward,
|
||||
*exp_reward,
|
||||
attacker_inventory,
|
||||
&mut attacker_skill_set,
|
||||
attacker_uid,
|
||||
@ -437,14 +447,53 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
let pos = state.ecs().read_storage::<comp::Pos>().get(entity).cloned();
|
||||
let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned();
|
||||
if let Some(pos) = pos {
|
||||
// TODO: This should only be temporary as you'd eventually want to actually
|
||||
// render the items on the ground, rather than changing the texture depending on
|
||||
// the body type
|
||||
let _ = state
|
||||
.create_item_drop(comp::Pos(pos.0 + Vec3::unit_z() * 0.25), &item)
|
||||
let winner_uid = if exp_awards.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// Use the awarded exp per entity as the weight distribution for drop chance
|
||||
// Creating the WeightedIndex can only fail if there are weights <= 0 or no
|
||||
// weights, which shouldn't ever happen
|
||||
let dist = WeightedIndex::new(exp_awards.iter().map(|x| x.1))
|
||||
.expect("Failed to create WeightedIndex for loot drop chance");
|
||||
let mut rng = rand::thread_rng();
|
||||
let winner = exp_awards
|
||||
.get(dist.sample(&mut rng))
|
||||
.expect("Loot distribution failed to find a winner")
|
||||
.0;
|
||||
|
||||
state
|
||||
.ecs()
|
||||
.read_storage::<comp::Body>()
|
||||
.get(winner)
|
||||
.and_then(|body| {
|
||||
// Only humanoids are awarded loot ownership - if the winner was a
|
||||
// non-humanoid NPC the loot will be free-for-all
|
||||
if matches!(body, Body::Humanoid(_)) {
|
||||
Some(state.ecs().read_storage::<Uid>().get(winner).cloned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
};
|
||||
|
||||
let item_drop_entity = state
|
||||
.create_item_drop(comp::Pos(pos.0 + Vec3::unit_z() * 0.25), item)
|
||||
.maybe_with(vel)
|
||||
.with(item)
|
||||
.build();
|
||||
|
||||
// If there was a loot winner, assign them as the owner of the loot. There will
|
||||
// not be a loot winner when an entity dies to environment damage and such so
|
||||
// the loot will be free-for-all.
|
||||
if let Some(uid) = winner_uid {
|
||||
debug!("Assigned UID {:?} as the winner for the loot drop", uid);
|
||||
|
||||
state
|
||||
.ecs()
|
||||
.write_storage::<comp::LootOwner>()
|
||||
.insert(item_drop_entity, LootOwner::new(uid))
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
?entity,
|
||||
|
@ -233,9 +233,8 @@ pub fn handle_mine_block(
|
||||
}
|
||||
}
|
||||
state
|
||||
.create_item_drop(Default::default(), &item)
|
||||
.create_item_drop(Default::default(), item)
|
||||
.with(comp::Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)))
|
||||
.with(item)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,10 @@ use comp::LightEmitter;
|
||||
|
||||
use crate::{client::Client, Server, StateExt};
|
||||
use common::{
|
||||
comp::{pet::is_tameable, ChatType, Group},
|
||||
comp::{
|
||||
pet::is_tameable, Alignment, Body, ChatType, CollectFailedReason, Group,
|
||||
InventoryUpdateEvent, Player,
|
||||
},
|
||||
event::{EventBus, ServerEvent},
|
||||
};
|
||||
use common_net::msg::ServerGeneral;
|
||||
@ -109,14 +112,15 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
}
|
||||
|
||||
match manip {
|
||||
comp::InventoryManip::Pickup(uid) => {
|
||||
let item_entity = if let Some(item_entity) = state.ecs().entity_from_uid(uid.into()) {
|
||||
comp::InventoryManip::Pickup(pickup_uid) => {
|
||||
let item_entity =
|
||||
if let Some(item_entity) = state.ecs().entity_from_uid(pickup_uid.into()) {
|
||||
item_entity
|
||||
} else {
|
||||
// Item entity could not be found - most likely because the entity
|
||||
// attempted to pick up the same item very quickly before its deletion of the
|
||||
// world from the first pickup attempt was processed.
|
||||
debug!("Failed to get entity for item Uid: {}", uid);
|
||||
debug!("Failed to get entity for item Uid: {}", pickup_uid);
|
||||
return;
|
||||
};
|
||||
let entity_cylinder = get_cylinder(state, entity);
|
||||
@ -125,11 +129,49 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
if !within_pickup_range(entity_cylinder, || get_cylinder(state, item_entity)) {
|
||||
debug!(
|
||||
?entity_cylinder,
|
||||
"Failed to pick up item as not within range, Uid: {}", uid
|
||||
"Failed to pick up item as not within range, Uid: {}", pickup_uid
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let loot_owner_storage = state.ecs().read_storage::<comp::LootOwner>();
|
||||
|
||||
// If there's a loot owner for the item being picked up, then
|
||||
// determine whether the pickup should be rejected.
|
||||
let ownership_check_passed = state
|
||||
.ecs()
|
||||
.read_storage::<comp::LootOwner>()
|
||||
.get(item_entity)
|
||||
.map_or(true, |loot_owner| {
|
||||
let alignments = state.ecs().read_storage::<Alignment>();
|
||||
let bodies = state.ecs().read_storage::<Body>();
|
||||
let players = state.ecs().read_storage::<Player>();
|
||||
let can_pickup = loot_owner.can_pickup(
|
||||
uid,
|
||||
alignments.get(entity),
|
||||
bodies.get(entity),
|
||||
players.get(entity),
|
||||
);
|
||||
if !can_pickup {
|
||||
let event =
|
||||
comp::InventoryUpdate::new(InventoryUpdateEvent::EntityCollectFailed {
|
||||
entity: pickup_uid,
|
||||
reason: CollectFailedReason::LootOwned {
|
||||
owner_uid: loot_owner.uid(),
|
||||
expiry_secs: loot_owner.time_until_expiration().as_secs(),
|
||||
},
|
||||
});
|
||||
state.ecs().write_storage().insert(entity, event).unwrap();
|
||||
}
|
||||
can_pickup
|
||||
});
|
||||
|
||||
if !ownership_check_passed {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(loot_owner_storage);
|
||||
|
||||
// First, we remove the item, assuming picking it up will succeed (we do this to
|
||||
// avoid cloning the item, as we should not call Item::clone and it
|
||||
// may be removed!).
|
||||
@ -140,7 +182,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
// Item component could not be found - most likely because the entity
|
||||
// attempted to pick up the same item very quickly before its deletion of the
|
||||
// world from the first pickup attempt was processed.
|
||||
debug!("Failed to delete item component for entity, Uid: {}", uid);
|
||||
debug!(
|
||||
"Failed to delete item component for entity, Uid: {}",
|
||||
pickup_uid
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -165,7 +210,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
);
|
||||
drop(item_storage);
|
||||
drop(inventories);
|
||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::EntityCollectFailed(uid))
|
||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::EntityCollectFailed {
|
||||
entity: pickup_uid,
|
||||
reason: comp::CollectFailedReason::InventoryFull,
|
||||
})
|
||||
},
|
||||
Ok(_) => {
|
||||
// We succeeded in picking up the item, so we may now delete its old entity
|
||||
@ -219,7 +267,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
Err(_) => {
|
||||
drop_item = Some(item_msg);
|
||||
comp::InventoryUpdate::new(
|
||||
comp::InventoryUpdateEvent::BlockCollectFailed(pos),
|
||||
comp::InventoryUpdateEvent::BlockCollectFailed {
|
||||
pos,
|
||||
reason: comp::CollectFailedReason::InventoryFull,
|
||||
},
|
||||
)
|
||||
},
|
||||
};
|
||||
@ -248,11 +299,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
drop(inventories);
|
||||
if let Some(item) = drop_item {
|
||||
state
|
||||
.create_item_drop(Default::default(), &item)
|
||||
.create_item_drop(Default::default(), item)
|
||||
.with(comp::Pos(
|
||||
Vec3::new(pos.x as f32, pos.y as f32, pos.z as f32) + Vec3::unit_z(),
|
||||
))
|
||||
.with(item)
|
||||
.with(comp::Vel(Vec3::zero()))
|
||||
.build();
|
||||
}
|
||||
@ -771,9 +821,8 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
});
|
||||
|
||||
state
|
||||
.create_item_drop(Default::default(), &item)
|
||||
.create_item_drop(Default::default(), item)
|
||||
.with(comp::Pos(pos.0 + *ori.look_dir() + Vec3::unit_z()))
|
||||
.with(item)
|
||||
.with(comp::Vel(Vec3::zero()))
|
||||
.build();
|
||||
}
|
||||
|
@ -2,14 +2,15 @@
|
||||
#![allow(clippy::option_map_unit_fn)]
|
||||
#![deny(clippy::clone_on_ref_ptr)]
|
||||
#![feature(
|
||||
box_patterns,
|
||||
label_break_value,
|
||||
bool_to_option,
|
||||
box_patterns,
|
||||
drain_filter,
|
||||
label_break_value,
|
||||
let_chains,
|
||||
let_else,
|
||||
never_type,
|
||||
option_zip,
|
||||
unwrap_infallible,
|
||||
let_else
|
||||
unwrap_infallible
|
||||
)]
|
||||
#![cfg_attr(not(feature = "worldgen"), feature(const_panic))]
|
||||
|
||||
|
@ -55,7 +55,7 @@ pub trait StateExt {
|
||||
) -> EcsEntityBuilder;
|
||||
/// Build a static object entity
|
||||
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
|
||||
fn create_item_drop(&mut self, pos: comp::Pos, item: &Item) -> EcsEntityBuilder;
|
||||
fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder;
|
||||
fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>(
|
||||
&mut self,
|
||||
pos: comp::Pos,
|
||||
@ -271,11 +271,12 @@ impl StateExt for State {
|
||||
.with(body)
|
||||
}
|
||||
|
||||
fn create_item_drop(&mut self, pos: comp::Pos, item: &Item) -> EcsEntityBuilder {
|
||||
let item_drop = comp::item_drop::Body::from(item);
|
||||
fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder {
|
||||
let item_drop = comp::item_drop::Body::from(&item);
|
||||
let body = comp::Body::ItemDrop(item_drop);
|
||||
self.ecs_mut()
|
||||
.create_entity_synced()
|
||||
.with(item)
|
||||
.with(pos)
|
||||
.with(comp::Vel(Vec3::zero()))
|
||||
.with(item_drop.orientation(&mut thread_rng()))
|
||||
|
@ -1603,22 +1603,29 @@ impl<'a> AgentData<'a> {
|
||||
};
|
||||
let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
|
||||
Some(Body::ItemDrop(item)) => {
|
||||
//If statement that checks either if the self (agent) is a humanoid,
|
||||
//or if the self is not a humanoid, it checks whether or not you are 'hungry' -
|
||||
// meaning less than full health - and additionally checks if
|
||||
// the target entity is a consumable item. If it qualifies for
|
||||
// either being a humanoid or a hungry non-humanoid that likes consumables,
|
||||
// it will choose the item as its target.
|
||||
if matches!(self.body, Some(Body::Humanoid(_)))
|
||||
|| (self
|
||||
// If there is no LootOwner then the ItemDrop is a valid target, otherwise check
|
||||
// if the loot can be picked up
|
||||
read_data
|
||||
.loot_owners
|
||||
.get(entity)
|
||||
.map_or(Some((entity, false)), |loot_owner| {
|
||||
// Agents want to pick up items if they are humanoid, or are hungry and the
|
||||
// item is consumable
|
||||
let hungry = self
|
||||
.health
|
||||
.map_or(false, |health| health.current() < health.maximum())
|
||||
&& matches!(item, item_drop::Body::Consumable))
|
||||
{
|
||||
.map_or(false, |health| health.current() < health.maximum());
|
||||
let wants_pickup = matches!(self.body, Some(Body::Humanoid(_)))
|
||||
|| (hungry && matches!(item, item_drop::Body::Consumable));
|
||||
|
||||
let can_pickup =
|
||||
loot_owner.can_pickup(*self.uid, self.alignment, self.body, None);
|
||||
|
||||
if wants_pickup && can_pickup {
|
||||
Some((entity, false))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
},
|
||||
_ => {
|
||||
if read_data.healths.get(entity).map_or(false, |health| {
|
||||
|
@ -2,7 +2,8 @@ use crate::rtsim::Entity as RtSimData;
|
||||
use common::{
|
||||
comp::{
|
||||
buff::Buffs, group, ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy,
|
||||
Health, Inventory, LightEmitter, Ori, PhysicsState, Pos, Scale, SkillSet, Stats, Vel,
|
||||
Health, Inventory, LightEmitter, LootOwner, Ori, PhysicsState, Pos, Scale, SkillSet, Stats,
|
||||
Vel,
|
||||
},
|
||||
link::Is,
|
||||
mounting::Mount,
|
||||
@ -152,6 +153,7 @@ pub struct ReadData<'a> {
|
||||
pub buffs: ReadStorage<'a, Buffs>,
|
||||
pub combos: ReadStorage<'a, Combo>,
|
||||
pub active_abilities: ReadStorage<'a, ActiveAbilities>,
|
||||
pub loot_owners: ReadStorage<'a, LootOwner>,
|
||||
}
|
||||
|
||||
pub enum Path {
|
||||
|
42
server/src/sys/loot.rs
Normal file
42
server/src/sys/loot.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use common::{comp::LootOwner, uid::UidAllocator};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{saveload::MarkerAllocator, Entities, Entity, Join, Read, WriteStorage};
|
||||
use tracing::debug;
|
||||
|
||||
// This system manages loot that exists in the world
|
||||
#[derive(Default)]
|
||||
pub struct Sys;
|
||||
impl<'a> System<'a> for Sys {
|
||||
type SystemData = (
|
||||
Entities<'a>,
|
||||
WriteStorage<'a, LootOwner>,
|
||||
Read<'a, UidAllocator>,
|
||||
);
|
||||
|
||||
const NAME: &'static str = "loot";
|
||||
const ORIGIN: Origin = Origin::Server;
|
||||
const PHASE: Phase = Phase::Create;
|
||||
|
||||
fn run(_job: &mut Job<Self>, (entities, mut loot_owners, uid_allocator): Self::SystemData) {
|
||||
// Find and remove expired loot ownership. Loot ownership is expired when either
|
||||
// the expiry time has passed, or the owner entity no longer exists
|
||||
let expired = (&entities, &loot_owners)
|
||||
.join()
|
||||
.filter(|(_, loot_owner)| {
|
||||
loot_owner.expired()
|
||||
|| uid_allocator
|
||||
.retrieve_entity_internal(loot_owner.uid().into())
|
||||
.map_or(true, |entity| !entities.is_alive(entity))
|
||||
})
|
||||
.map(|(entity, _)| entity)
|
||||
.collect::<Vec<Entity>>();
|
||||
|
||||
if !&expired.is_empty() {
|
||||
debug!("Removing {} expired loot ownerships", expired.iter().len());
|
||||
}
|
||||
|
||||
for entity in expired {
|
||||
loot_owners.remove(entity);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ pub mod chunk_send;
|
||||
pub mod chunk_serialize;
|
||||
pub mod entity_sync;
|
||||
pub mod invite_timeout;
|
||||
pub mod loot;
|
||||
pub mod metrics;
|
||||
pub mod msg;
|
||||
pub mod object;
|
||||
|
@ -5,7 +5,10 @@ pub mod ping;
|
||||
pub mod register;
|
||||
pub mod terrain;
|
||||
|
||||
use crate::{client::Client, sys::pets};
|
||||
use crate::{
|
||||
client::Client,
|
||||
sys::{loot, pets},
|
||||
};
|
||||
use common_ecs::{dispatch, System};
|
||||
use serde::de::DeserializeOwned;
|
||||
use specs::DispatcherBuilder;
|
||||
@ -20,6 +23,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
||||
dispatch::<register::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<terrain::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<pets::Sys>(dispatch_builder, &[]);
|
||||
dispatch::<loot::Sys>(dispatch_builder, &[]);
|
||||
}
|
||||
|
||||
/// handles all send msg and calls a handle fn
|
||||
|
@ -314,8 +314,8 @@ impl From<&InventoryUpdateEvent> for SfxEvent {
|
||||
_ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
|
||||
}
|
||||
},
|
||||
InventoryUpdateEvent::BlockCollectFailed(_)
|
||||
| InventoryUpdateEvent::EntityCollectFailed(_) => {
|
||||
InventoryUpdateEvent::BlockCollectFailed { .. }
|
||||
| InventoryUpdateEvent::EntityCollectFailed { .. } => {
|
||||
SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
|
||||
},
|
||||
InventoryUpdateEvent::Consumed(consumable) => {
|
||||
|
@ -80,7 +80,7 @@ use common::{
|
||||
self,
|
||||
ability::AuxiliaryAbility,
|
||||
fluid_dynamics,
|
||||
inventory::{slot::InvSlotId, trade_pricing::TradePricing},
|
||||
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
|
||||
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
|
||||
pet::is_mountable,
|
||||
skillset::{skills::Skill, SkillGroupKind},
|
||||
@ -997,6 +997,58 @@ pub struct Floaters {
|
||||
pub block_floaters: Vec<BlockFloater>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum HudLootOwner {
|
||||
Name(String),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum HudCollectFailedReason {
|
||||
InventoryFull,
|
||||
LootOwned {
|
||||
owner: HudLootOwner,
|
||||
expiry_secs: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl HudCollectFailedReason {
|
||||
pub fn from_server_reason(reason: &CollectFailedReason, ecs: &specs::World) -> Self {
|
||||
match reason {
|
||||
CollectFailedReason::InventoryFull => HudCollectFailedReason::InventoryFull,
|
||||
CollectFailedReason::LootOwned {
|
||||
owner_uid,
|
||||
expiry_secs,
|
||||
} => {
|
||||
let maybe_owner_name =
|
||||
ecs.entity_from_uid((*owner_uid).into()).and_then(|entity| {
|
||||
ecs.read_storage::<comp::Stats>()
|
||||
.get(entity)
|
||||
.map(|stats| stats.name.clone())
|
||||
});
|
||||
let owner = if let Some(name) = maybe_owner_name {
|
||||
HudLootOwner::Name(name)
|
||||
} else {
|
||||
HudLootOwner::Unknown
|
||||
};
|
||||
HudCollectFailedReason::LootOwned {
|
||||
owner,
|
||||
expiry_secs: *expiry_secs,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct CollectFailedData {
|
||||
pulse: f32,
|
||||
reason: HudCollectFailedReason,
|
||||
}
|
||||
|
||||
impl CollectFailedData {
|
||||
pub fn new(pulse: f32, reason: HudCollectFailedReason) -> Self { Self { pulse, reason } }
|
||||
}
|
||||
|
||||
pub struct Hud {
|
||||
ui: Ui,
|
||||
ids: Ids,
|
||||
@ -1005,8 +1057,8 @@ pub struct Hud {
|
||||
item_imgs: ItemImgs,
|
||||
fonts: Fonts,
|
||||
rot_imgs: ImgsRot,
|
||||
failed_block_pickups: HashMap<Vec3<i32>, f32>,
|
||||
failed_entity_pickups: HashMap<EcsEntity, f32>,
|
||||
failed_block_pickups: HashMap<Vec3<i32>, CollectFailedData>,
|
||||
failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
|
||||
new_loot_messages: VecDeque<LootMessage>,
|
||||
new_messages: VecDeque<comp::ChatMsg>,
|
||||
new_notifications: VecDeque<Notification>,
|
||||
@ -1772,9 +1824,9 @@ impl Hud {
|
||||
};
|
||||
|
||||
self.failed_block_pickups
|
||||
.retain(|_, t| pulse - *t < overitem::PICKUP_FAILED_FADE_OUT_TIME);
|
||||
.retain(|_, t| pulse - (*t).pulse < overitem::PICKUP_FAILED_FADE_OUT_TIME);
|
||||
self.failed_entity_pickups
|
||||
.retain(|_, t| pulse - *t < overitem::PICKUP_FAILED_FADE_OUT_TIME);
|
||||
.retain(|_, t| pulse - (*t).pulse < overitem::PICKUP_FAILED_FADE_OUT_TIME);
|
||||
|
||||
// Render overitem: name, etc.
|
||||
for (entity, pos, item, distance) in (&entities, &pos, &items)
|
||||
@ -1793,7 +1845,7 @@ impl Hud {
|
||||
distance,
|
||||
overitem::OveritemProperties {
|
||||
active: interactable.as_ref().and_then(|i| i.entity()) == Some(entity),
|
||||
pickup_failed_pulse: self.failed_entity_pickups.get(&entity).copied(),
|
||||
pickup_failed_pulse: self.failed_entity_pickups.get(&entity).cloned(),
|
||||
},
|
||||
&self.fonts,
|
||||
vec![(GameInput::Interact, i18n.get("hud.pick_up").to_string())],
|
||||
@ -1810,7 +1862,7 @@ impl Hud {
|
||||
|
||||
let overitem_properties = overitem::OveritemProperties {
|
||||
active: true,
|
||||
pickup_failed_pulse: self.failed_block_pickups.get(&pos).copied(),
|
||||
pickup_failed_pulse: self.failed_block_pickups.get(&pos).cloned(),
|
||||
};
|
||||
let pos = pos.map(|e| e as f32 + 0.5);
|
||||
let over_pos = pos + Vec3::unit_z() * 0.7;
|
||||
@ -3938,12 +3990,14 @@ impl Hud {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_failed_block_pickup(&mut self, pos: Vec3<i32>) {
|
||||
self.failed_block_pickups.insert(pos, self.pulse);
|
||||
pub fn add_failed_block_pickup(&mut self, pos: Vec3<i32>, reason: HudCollectFailedReason) {
|
||||
self.failed_block_pickups
|
||||
.insert(pos, CollectFailedData::new(self.pulse, reason));
|
||||
}
|
||||
|
||||
pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity) {
|
||||
self.failed_entity_pickups.insert(entity, self.pulse);
|
||||
pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity, reason: HudCollectFailedReason) {
|
||||
self.failed_entity_pickups
|
||||
.insert(entity, CollectFailedData::new(self.pulse, reason));
|
||||
}
|
||||
|
||||
pub fn new_loot_message(&mut self, item: LootMessage) {
|
||||
|
@ -11,6 +11,7 @@ use conrod_core::{
|
||||
use i18n::Localization;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::hud::{CollectFailedData, HudCollectFailedReason, HudLootOwner};
|
||||
use keyboard_keynames::key_layout::KeyLayout;
|
||||
|
||||
pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0);
|
||||
@ -79,7 +80,7 @@ impl<'a> Overitem<'a> {
|
||||
|
||||
pub struct OveritemProperties {
|
||||
pub active: bool,
|
||||
pub pickup_failed_pulse: Option<f32>,
|
||||
pub pickup_failed_pulse: Option<CollectFailedData>,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
@ -215,9 +216,10 @@ impl<'a> Widget for Overitem<'a> {
|
||||
.parent(id)
|
||||
.set(state.ids.btn_bg, ui);
|
||||
}
|
||||
if let Some(time) = self.properties.pickup_failed_pulse {
|
||||
if let Some(collect_failed_data) = self.properties.pickup_failed_pulse {
|
||||
//should never exceed 1.0, but just in case
|
||||
let age = ((self.pulse - time) / PICKUP_FAILED_FADE_OUT_TIME).clamp(0.0, 1.0);
|
||||
let age = ((self.pulse - collect_failed_data.pulse) / PICKUP_FAILED_FADE_OUT_TIME)
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
let alpha = 1.0 - age.powi(4);
|
||||
let brightness = 1.0 / (age / 0.07 - 1.0).abs().clamp(0.01, 1.0);
|
||||
@ -226,7 +228,25 @@ impl<'a> Widget for Overitem<'a> {
|
||||
color::hsla(hue, sat / brightness, lum * brightness.sqrt(), alp * alpha)
|
||||
};
|
||||
|
||||
Text::new(self.localized_strings.get("hud.inventory_full"))
|
||||
let text = match collect_failed_data.reason {
|
||||
HudCollectFailedReason::InventoryFull => {
|
||||
self.localized_strings.get("hud.inventory_full").to_string()
|
||||
},
|
||||
HudCollectFailedReason::LootOwned { owner, expiry_secs } => {
|
||||
let owner_name = match owner {
|
||||
HudLootOwner::Name(name) => name,
|
||||
HudLootOwner::Unknown => {
|
||||
self.localized_strings.get("hud.someone_else").to_string()
|
||||
},
|
||||
};
|
||||
self.localized_strings
|
||||
.get("hud.owned_by_for_secs")
|
||||
.replace("{name}", &owner_name)
|
||||
.replace("{secs}", format!("{}", expiry_secs).as_str())
|
||||
},
|
||||
};
|
||||
|
||||
Text::new(&text)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(inv_full_font_size as u32)
|
||||
.color(shade_color(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
|
||||
@ -235,7 +255,7 @@ impl<'a> Widget for Overitem<'a> {
|
||||
.depth(self.distance_from_player_sqr + 6.0)
|
||||
.set(state.ids.inv_full_bg, ui);
|
||||
|
||||
Text::new(self.localized_strings.get("hud.inventory_full"))
|
||||
Text::new(&text)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(inv_full_font_size as u32)
|
||||
.color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0)))
|
||||
|
@ -43,7 +43,10 @@ use crate::{
|
||||
audio::sfx::SfxEvent,
|
||||
error::Error,
|
||||
game_input::GameInput,
|
||||
hud::{DebugInfo, Event as HudEvent, Hud, HudInfo, LootMessage, PromptDialogSettings},
|
||||
hud::{
|
||||
DebugInfo, Event as HudEvent, Hud, HudCollectFailedReason, HudInfo, LootMessage,
|
||||
PromptDialogSettings,
|
||||
},
|
||||
key_state::KeyState,
|
||||
menu::char_selection::CharSelectionState,
|
||||
render::{Drawer, GlobalsBindGroup},
|
||||
@ -244,12 +247,27 @@ impl SessionState {
|
||||
global_state.audio.emit_sfx_item(sfx_trigger_item);
|
||||
|
||||
match inv_event {
|
||||
InventoryUpdateEvent::BlockCollectFailed(pos) => {
|
||||
self.hud.add_failed_block_pickup(pos);
|
||||
InventoryUpdateEvent::BlockCollectFailed { pos, reason } => {
|
||||
self.hud.add_failed_block_pickup(
|
||||
pos,
|
||||
HudCollectFailedReason::from_server_reason(
|
||||
&reason,
|
||||
client.state().ecs(),
|
||||
),
|
||||
);
|
||||
},
|
||||
InventoryUpdateEvent::EntityCollectFailed(uid) => {
|
||||
InventoryUpdateEvent::EntityCollectFailed {
|
||||
entity: uid,
|
||||
reason,
|
||||
} => {
|
||||
if let Some(entity) = client.state().ecs().entity_from_uid(uid.into()) {
|
||||
self.hud.add_failed_entity_pickup(entity);
|
||||
self.hud.add_failed_entity_pickup(
|
||||
entity,
|
||||
HudCollectFailedReason::from_server_reason(
|
||||
&reason,
|
||||
client.state().ecs(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
InventoryUpdateEvent::Collected(item) => {
|
||||
|
Loading…
Reference in New Issue
Block a user