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
|
- Modular weapons
|
||||||
- Added Thai translation
|
- Added Thai translation
|
||||||
- Skiing and ice skating
|
- Skiing and ice skating
|
||||||
|
- Added loot ownership for NPC drops
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
"hud.waypoint_saved": "Waypoint Saved",
|
"hud.waypoint_saved": "Waypoint Saved",
|
||||||
"hud.sp_arrow_txt": "SP",
|
"hud.sp_arrow_txt": "SP",
|
||||||
"hud.inventory_full": "Inventory Full",
|
"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_show_keybindings_fmt": "[{key}] Keybindings",
|
||||||
"hud.press_key_to_toggle_lantern_fmt": "[{key}] Lantern",
|
"hud.press_key_to_toggle_lantern_fmt": "[{key}] Lantern",
|
||||||
|
@ -2133,8 +2133,8 @@ impl Client {
|
|||||||
},
|
},
|
||||||
ServerGeneral::InventoryUpdate(inventory, event) => {
|
ServerGeneral::InventoryUpdate(inventory, event) => {
|
||||||
match event {
|
match event {
|
||||||
InventoryUpdateEvent::BlockCollectFailed(_) => {},
|
InventoryUpdateEvent::BlockCollectFailed { .. } => {},
|
||||||
InventoryUpdateEvent::EntityCollectFailed(_) => {},
|
InventoryUpdateEvent::EntityCollectFailed { .. } => {},
|
||||||
_ => {
|
_ => {
|
||||||
// Push the updated inventory component to the client
|
// Push the updated inventory component to the client
|
||||||
// FIXME: Figure out whether this error can happen under normal gameplay,
|
// 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()
|
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> {
|
fn entity_from_uid(&self, uid: u64) -> Option<specs::Entity> {
|
||||||
self.read_resource::<UidAllocator>()
|
self.read_resource::<UidAllocator>()
|
||||||
.retrieve_entity_internal(uid)
|
.retrieve_entity_internal(uid)
|
||||||
|
@ -62,6 +62,7 @@ macro_rules! synced_components {
|
|||||||
combo: Combo,
|
combo: Combo,
|
||||||
active_abilities: ActiveAbilities,
|
active_abilities: ActiveAbilities,
|
||||||
can_build: CanBuild,
|
can_build: CanBuild,
|
||||||
|
loot_owner: LootOwner,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -234,3 +235,7 @@ impl NetSync for ActiveAbilities {
|
|||||||
impl NetSync for CanBuild {
|
impl NetSync for CanBuild {
|
||||||
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
|
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>>;
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum InventoryUpdateEvent {
|
pub enum InventoryUpdateEvent {
|
||||||
Init,
|
Init,
|
||||||
@ -813,8 +819,14 @@ pub enum InventoryUpdateEvent {
|
|||||||
Swapped,
|
Swapped,
|
||||||
Dropped,
|
Dropped,
|
||||||
Collected(Item),
|
Collected(Item),
|
||||||
BlockCollectFailed(Vec3<i32>),
|
BlockCollectFailed {
|
||||||
EntityCollectFailed(Uid),
|
pos: Vec3<i32>,
|
||||||
|
reason: CollectFailedReason,
|
||||||
|
},
|
||||||
|
EntityCollectFailed {
|
||||||
|
entity: Uid,
|
||||||
|
reason: CollectFailedReason,
|
||||||
|
},
|
||||||
Possession,
|
Possession,
|
||||||
Debug,
|
Debug,
|
||||||
Craft,
|
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;
|
pub mod invite;
|
||||||
#[cfg(not(target_arch = "wasm32"))] mod last;
|
#[cfg(not(target_arch = "wasm32"))] mod last;
|
||||||
#[cfg(not(target_arch = "wasm32"))] mod location;
|
#[cfg(not(target_arch = "wasm32"))] mod location;
|
||||||
|
pub mod loot_owner;
|
||||||
#[cfg(not(target_arch = "wasm32"))] pub mod melee;
|
#[cfg(not(target_arch = "wasm32"))] pub mod melee;
|
||||||
#[cfg(not(target_arch = "wasm32"))] mod misc;
|
#[cfg(not(target_arch = "wasm32"))] mod misc;
|
||||||
#[cfg(not(target_arch = "wasm32"))] pub mod ori;
|
#[cfg(not(target_arch = "wasm32"))] pub mod ori;
|
||||||
@ -87,10 +88,11 @@ pub use self::{
|
|||||||
tool::{self, AbilityItem},
|
tool::{self, AbilityItem},
|
||||||
Item, ItemConfig, ItemDrop,
|
Item, ItemConfig, ItemDrop,
|
||||||
},
|
},
|
||||||
slot, Inventory, InventoryUpdate, InventoryUpdateEvent,
|
slot, CollectFailedReason, Inventory, InventoryUpdate, InventoryUpdateEvent,
|
||||||
},
|
},
|
||||||
last::Last,
|
last::Last,
|
||||||
location::{MapMarker, MapMarkerChange, MapMarkerUpdate, Waypoint, WaypointArea},
|
location::{MapMarker, MapMarkerChange, MapMarkerUpdate, Waypoint, WaypointArea},
|
||||||
|
loot_owner::LootOwner,
|
||||||
melee::{Melee, MeleeConstructor},
|
melee::{Melee, MeleeConstructor},
|
||||||
misc::Object,
|
misc::Object,
|
||||||
ori::Ori,
|
ori::Ori,
|
||||||
|
@ -157,6 +157,7 @@ impl State {
|
|||||||
ecs.register::<comp::ShockwaveHitEntities>();
|
ecs.register::<comp::ShockwaveHitEntities>();
|
||||||
ecs.register::<comp::BeamSegment>();
|
ecs.register::<comp::BeamSegment>();
|
||||||
ecs.register::<comp::Alignment>();
|
ecs.register::<comp::Alignment>();
|
||||||
|
ecs.register::<comp::LootOwner>();
|
||||||
|
|
||||||
// Register components send from clients -> server
|
// Register components send from clients -> server
|
||||||
ecs.register::<comp::Controller>();
|
ecs.register::<comp::Controller>();
|
||||||
|
@ -500,13 +500,12 @@ fn handle_drop_all(
|
|||||||
|
|
||||||
server
|
server
|
||||||
.state
|
.state
|
||||||
.create_item_drop(Default::default(), &item)
|
.create_item_drop(Default::default(), item)
|
||||||
.with(comp::Pos(Vec3::new(
|
.with(comp::Pos(Vec3::new(
|
||||||
pos.0.x + rng.gen_range(5.0..10.0),
|
pos.0.x + rng.gen_range(5.0..10.0),
|
||||||
pos.0.y + rng.gen_range(5.0..10.0),
|
pos.0.y + rng.gen_range(5.0..10.0),
|
||||||
pos.0.z + 5.0,
|
pos.0.z + 5.0,
|
||||||
)))
|
)))
|
||||||
.with(item)
|
|
||||||
.with(comp::Vel(vel))
|
.with(comp::Vel(vel))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
comp::{
|
comp::{
|
||||||
ability,
|
ability,
|
||||||
agent::{Agent, AgentEvent, Sound, SoundKind},
|
agent::{Agent, AgentEvent, Sound, SoundKind},
|
||||||
|
loot_owner::LootOwner,
|
||||||
skillset::SkillGroupKind,
|
skillset::SkillGroupKind,
|
||||||
BuffKind, BuffSource, PhysicsState,
|
BuffKind, BuffSource, PhysicsState,
|
||||||
},
|
},
|
||||||
@ -34,7 +35,8 @@ use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
|
|||||||
use common_state::BlockChange;
|
use common_state::BlockChange;
|
||||||
use comp::chat::GenericChatMsg;
|
use comp::chat::GenericChatMsg;
|
||||||
use hashbrown::HashSet;
|
use hashbrown::HashSet;
|
||||||
use rand::Rng;
|
use rand::{distributions::WeightedIndex, Rng};
|
||||||
|
use rand_distr::Distribution;
|
||||||
use specs::{
|
use specs::{
|
||||||
join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
|
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
|
// Award EXP to damage contributors
|
||||||
//
|
//
|
||||||
// NOTE: Debug logging is disabled by default for this module - to enable it add
|
// 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
|
// Iterate through all contributors of damage for the killed entity, calculating
|
||||||
// how much EXP each contributor should be awarded based on their
|
// how much EXP each contributor should be awarded based on their
|
||||||
// percentage of damage contribution
|
// 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;
|
let contributor_exp = exp_reward * damage_percent;
|
||||||
match damage_contributor {
|
match damage_contributor {
|
||||||
DamageContrib::Solo(attacker) => {
|
DamageContrib::Solo(attacker) => {
|
||||||
@ -365,16 +368,23 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
|||||||
None
|
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
|
// Process the calculated EXP rewards
|
||||||
if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory), Some(pos)) = (
|
if let (
|
||||||
skill_sets.get_mut(attacker),
|
Some(mut attacker_skill_set),
|
||||||
uids.get(attacker),
|
Some(attacker_uid),
|
||||||
inventories.get(attacker),
|
Some(attacker_inventory),
|
||||||
positions.get(attacker),
|
Some(pos),
|
||||||
|
) = (
|
||||||
|
skill_sets.get_mut(*attacker),
|
||||||
|
uids.get(*attacker),
|
||||||
|
inventories.get(*attacker),
|
||||||
|
positions.get(*attacker),
|
||||||
) {
|
) {
|
||||||
handle_exp_gain(
|
handle_exp_gain(
|
||||||
exp_reward,
|
*exp_reward,
|
||||||
attacker_inventory,
|
attacker_inventory,
|
||||||
&mut attacker_skill_set,
|
&mut attacker_skill_set,
|
||||||
attacker_uid,
|
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 pos = state.ecs().read_storage::<comp::Pos>().get(entity).cloned();
|
||||||
let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned();
|
let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned();
|
||||||
if let Some(pos) = pos {
|
if let Some(pos) = pos {
|
||||||
// TODO: This should only be temporary as you'd eventually want to actually
|
let winner_uid = if exp_awards.is_empty() {
|
||||||
// render the items on the ground, rather than changing the texture depending on
|
None
|
||||||
// the body type
|
} else {
|
||||||
let _ = state
|
// Use the awarded exp per entity as the weight distribution for drop chance
|
||||||
.create_item_drop(comp::Pos(pos.0 + Vec3::unit_z() * 0.25), &item)
|
// 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)
|
.maybe_with(vel)
|
||||||
.with(item)
|
|
||||||
.build();
|
.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 {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
?entity,
|
?entity,
|
||||||
|
@ -233,9 +233,8 @@ pub fn handle_mine_block(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
state
|
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(comp::Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)))
|
||||||
.with(item)
|
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,10 @@ use comp::LightEmitter;
|
|||||||
|
|
||||||
use crate::{client::Client, Server, StateExt};
|
use crate::{client::Client, Server, StateExt};
|
||||||
use common::{
|
use common::{
|
||||||
comp::{pet::is_tameable, ChatType, Group},
|
comp::{
|
||||||
|
pet::is_tameable, Alignment, Body, ChatType, CollectFailedReason, Group,
|
||||||
|
InventoryUpdateEvent, Player,
|
||||||
|
},
|
||||||
event::{EventBus, ServerEvent},
|
event::{EventBus, ServerEvent},
|
||||||
};
|
};
|
||||||
use common_net::msg::ServerGeneral;
|
use common_net::msg::ServerGeneral;
|
||||||
@ -109,27 +112,66 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
}
|
}
|
||||||
|
|
||||||
match manip {
|
match manip {
|
||||||
comp::InventoryManip::Pickup(uid) => {
|
comp::InventoryManip::Pickup(pickup_uid) => {
|
||||||
let item_entity = if let Some(item_entity) = state.ecs().entity_from_uid(uid.into()) {
|
let item_entity =
|
||||||
item_entity
|
if let Some(item_entity) = state.ecs().entity_from_uid(pickup_uid.into()) {
|
||||||
} else {
|
item_entity
|
||||||
// Item entity could not be found - most likely because the entity
|
} else {
|
||||||
// attempted to pick up the same item very quickly before its deletion of the
|
// Item entity could not be found - most likely because the entity
|
||||||
// world from the first pickup attempt was processed.
|
// attempted to pick up the same item very quickly before its deletion of the
|
||||||
debug!("Failed to get entity for item Uid: {}", uid);
|
// world from the first pickup attempt was processed.
|
||||||
return;
|
debug!("Failed to get entity for item Uid: {}", pickup_uid);
|
||||||
};
|
return;
|
||||||
|
};
|
||||||
let entity_cylinder = get_cylinder(state, entity);
|
let entity_cylinder = get_cylinder(state, entity);
|
||||||
|
|
||||||
// FIXME: Raycast so we can't pick up items through walls.
|
// FIXME: Raycast so we can't pick up items through walls.
|
||||||
if !within_pickup_range(entity_cylinder, || get_cylinder(state, item_entity)) {
|
if !within_pickup_range(entity_cylinder, || get_cylinder(state, item_entity)) {
|
||||||
debug!(
|
debug!(
|
||||||
?entity_cylinder,
|
?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;
|
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
|
// 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
|
// avoid cloning the item, as we should not call Item::clone and it
|
||||||
// may be removed!).
|
// 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
|
// 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
|
// attempted to pick up the same item very quickly before its deletion of the
|
||||||
// world from the first pickup attempt was processed.
|
// 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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,7 +210,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
);
|
);
|
||||||
drop(item_storage);
|
drop(item_storage);
|
||||||
drop(inventories);
|
drop(inventories);
|
||||||
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::EntityCollectFailed(uid))
|
comp::InventoryUpdate::new(comp::InventoryUpdateEvent::EntityCollectFailed {
|
||||||
|
entity: pickup_uid,
|
||||||
|
reason: comp::CollectFailedReason::InventoryFull,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// We succeeded in picking up the item, so we may now delete its old entity
|
// 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(_) => {
|
Err(_) => {
|
||||||
drop_item = Some(item_msg);
|
drop_item = Some(item_msg);
|
||||||
comp::InventoryUpdate::new(
|
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);
|
drop(inventories);
|
||||||
if let Some(item) = drop_item {
|
if let Some(item) = drop_item {
|
||||||
state
|
state
|
||||||
.create_item_drop(Default::default(), &item)
|
.create_item_drop(Default::default(), item)
|
||||||
.with(comp::Pos(
|
.with(comp::Pos(
|
||||||
Vec3::new(pos.x as f32, pos.y as f32, pos.z as f32) + Vec3::unit_z(),
|
Vec3::new(pos.x as f32, pos.y as f32, pos.z as f32) + Vec3::unit_z(),
|
||||||
))
|
))
|
||||||
.with(item)
|
|
||||||
.with(comp::Vel(Vec3::zero()))
|
.with(comp::Vel(Vec3::zero()))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
@ -771,9 +821,8 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
|||||||
});
|
});
|
||||||
|
|
||||||
state
|
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(comp::Pos(pos.0 + *ori.look_dir() + Vec3::unit_z()))
|
||||||
.with(item)
|
|
||||||
.with(comp::Vel(Vec3::zero()))
|
.with(comp::Vel(Vec3::zero()))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
#![allow(clippy::option_map_unit_fn)]
|
#![allow(clippy::option_map_unit_fn)]
|
||||||
#![deny(clippy::clone_on_ref_ptr)]
|
#![deny(clippy::clone_on_ref_ptr)]
|
||||||
#![feature(
|
#![feature(
|
||||||
box_patterns,
|
|
||||||
label_break_value,
|
|
||||||
bool_to_option,
|
bool_to_option,
|
||||||
|
box_patterns,
|
||||||
drain_filter,
|
drain_filter,
|
||||||
|
label_break_value,
|
||||||
|
let_chains,
|
||||||
|
let_else,
|
||||||
never_type,
|
never_type,
|
||||||
option_zip,
|
option_zip,
|
||||||
unwrap_infallible,
|
unwrap_infallible
|
||||||
let_else
|
|
||||||
)]
|
)]
|
||||||
#![cfg_attr(not(feature = "worldgen"), feature(const_panic))]
|
#![cfg_attr(not(feature = "worldgen"), feature(const_panic))]
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ pub trait StateExt {
|
|||||||
) -> EcsEntityBuilder;
|
) -> EcsEntityBuilder;
|
||||||
/// Build a static object entity
|
/// Build a static object entity
|
||||||
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
|
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>(
|
fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>(
|
||||||
&mut self,
|
&mut self,
|
||||||
pos: comp::Pos,
|
pos: comp::Pos,
|
||||||
@ -271,11 +271,12 @@ impl StateExt for State {
|
|||||||
.with(body)
|
.with(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_item_drop(&mut self, pos: comp::Pos, item: &Item) -> EcsEntityBuilder {
|
fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder {
|
||||||
let item_drop = comp::item_drop::Body::from(item);
|
let item_drop = comp::item_drop::Body::from(&item);
|
||||||
let body = comp::Body::ItemDrop(item_drop);
|
let body = comp::Body::ItemDrop(item_drop);
|
||||||
self.ecs_mut()
|
self.ecs_mut()
|
||||||
.create_entity_synced()
|
.create_entity_synced()
|
||||||
|
.with(item)
|
||||||
.with(pos)
|
.with(pos)
|
||||||
.with(comp::Vel(Vec3::zero()))
|
.with(comp::Vel(Vec3::zero()))
|
||||||
.with(item_drop.orientation(&mut thread_rng()))
|
.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) {
|
let is_valid_target = |entity: EcsEntity| match read_data.bodies.get(entity) {
|
||||||
Some(Body::ItemDrop(item)) => {
|
Some(Body::ItemDrop(item)) => {
|
||||||
//If statement that checks either if the self (agent) is a humanoid,
|
// If there is no LootOwner then the ItemDrop is a valid target, otherwise check
|
||||||
//or if the self is not a humanoid, it checks whether or not you are 'hungry' -
|
// if the loot can be picked up
|
||||||
// meaning less than full health - and additionally checks if
|
read_data
|
||||||
// the target entity is a consumable item. If it qualifies for
|
.loot_owners
|
||||||
// either being a humanoid or a hungry non-humanoid that likes consumables,
|
.get(entity)
|
||||||
// it will choose the item as its target.
|
.map_or(Some((entity, false)), |loot_owner| {
|
||||||
if matches!(self.body, Some(Body::Humanoid(_)))
|
// Agents want to pick up items if they are humanoid, or are hungry and the
|
||||||
|| (self
|
// item is consumable
|
||||||
.health
|
let hungry = self
|
||||||
.map_or(false, |health| health.current() < health.maximum())
|
.health
|
||||||
&& matches!(item, item_drop::Body::Consumable))
|
.map_or(false, |health| health.current() < health.maximum());
|
||||||
{
|
let wants_pickup = matches!(self.body, Some(Body::Humanoid(_)))
|
||||||
Some((entity, false))
|
|| (hungry && matches!(item, item_drop::Body::Consumable));
|
||||||
} else {
|
|
||||||
None
|
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| {
|
if read_data.healths.get(entity).map_or(false, |health| {
|
||||||
|
@ -2,7 +2,8 @@ use crate::rtsim::Entity as RtSimData;
|
|||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
buff::Buffs, group, ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy,
|
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,
|
link::Is,
|
||||||
mounting::Mount,
|
mounting::Mount,
|
||||||
@ -152,6 +153,7 @@ pub struct ReadData<'a> {
|
|||||||
pub buffs: ReadStorage<'a, Buffs>,
|
pub buffs: ReadStorage<'a, Buffs>,
|
||||||
pub combos: ReadStorage<'a, Combo>,
|
pub combos: ReadStorage<'a, Combo>,
|
||||||
pub active_abilities: ReadStorage<'a, ActiveAbilities>,
|
pub active_abilities: ReadStorage<'a, ActiveAbilities>,
|
||||||
|
pub loot_owners: ReadStorage<'a, LootOwner>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Path {
|
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 chunk_serialize;
|
||||||
pub mod entity_sync;
|
pub mod entity_sync;
|
||||||
pub mod invite_timeout;
|
pub mod invite_timeout;
|
||||||
|
pub mod loot;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod msg;
|
pub mod msg;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
|
@ -5,7 +5,10 @@ pub mod ping;
|
|||||||
pub mod register;
|
pub mod register;
|
||||||
pub mod terrain;
|
pub mod terrain;
|
||||||
|
|
||||||
use crate::{client::Client, sys::pets};
|
use crate::{
|
||||||
|
client::Client,
|
||||||
|
sys::{loot, pets},
|
||||||
|
};
|
||||||
use common_ecs::{dispatch, System};
|
use common_ecs::{dispatch, System};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use specs::DispatcherBuilder;
|
use specs::DispatcherBuilder;
|
||||||
@ -20,6 +23,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
|
|||||||
dispatch::<register::Sys>(dispatch_builder, &[]);
|
dispatch::<register::Sys>(dispatch_builder, &[]);
|
||||||
dispatch::<terrain::Sys>(dispatch_builder, &[]);
|
dispatch::<terrain::Sys>(dispatch_builder, &[]);
|
||||||
dispatch::<pets::Sys>(dispatch_builder, &[]);
|
dispatch::<pets::Sys>(dispatch_builder, &[]);
|
||||||
|
dispatch::<loot::Sys>(dispatch_builder, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// handles all send msg and calls a handle fn
|
/// handles all send msg and calls a handle fn
|
||||||
|
@ -314,8 +314,8 @@ impl From<&InventoryUpdateEvent> for SfxEvent {
|
|||||||
_ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
|
_ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
InventoryUpdateEvent::BlockCollectFailed(_)
|
InventoryUpdateEvent::BlockCollectFailed { .. }
|
||||||
| InventoryUpdateEvent::EntityCollectFailed(_) => {
|
| InventoryUpdateEvent::EntityCollectFailed { .. } => {
|
||||||
SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
|
SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
|
||||||
},
|
},
|
||||||
InventoryUpdateEvent::Consumed(consumable) => {
|
InventoryUpdateEvent::Consumed(consumable) => {
|
||||||
|
@ -80,7 +80,7 @@ use common::{
|
|||||||
self,
|
self,
|
||||||
ability::AuxiliaryAbility,
|
ability::AuxiliaryAbility,
|
||||||
fluid_dynamics,
|
fluid_dynamics,
|
||||||
inventory::{slot::InvSlotId, trade_pricing::TradePricing},
|
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
|
||||||
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
|
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
|
||||||
pet::is_mountable,
|
pet::is_mountable,
|
||||||
skillset::{skills::Skill, SkillGroupKind},
|
skillset::{skills::Skill, SkillGroupKind},
|
||||||
@ -997,6 +997,58 @@ pub struct Floaters {
|
|||||||
pub block_floaters: Vec<BlockFloater>,
|
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 {
|
pub struct Hud {
|
||||||
ui: Ui,
|
ui: Ui,
|
||||||
ids: Ids,
|
ids: Ids,
|
||||||
@ -1005,8 +1057,8 @@ pub struct Hud {
|
|||||||
item_imgs: ItemImgs,
|
item_imgs: ItemImgs,
|
||||||
fonts: Fonts,
|
fonts: Fonts,
|
||||||
rot_imgs: ImgsRot,
|
rot_imgs: ImgsRot,
|
||||||
failed_block_pickups: HashMap<Vec3<i32>, f32>,
|
failed_block_pickups: HashMap<Vec3<i32>, CollectFailedData>,
|
||||||
failed_entity_pickups: HashMap<EcsEntity, f32>,
|
failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
|
||||||
new_loot_messages: VecDeque<LootMessage>,
|
new_loot_messages: VecDeque<LootMessage>,
|
||||||
new_messages: VecDeque<comp::ChatMsg>,
|
new_messages: VecDeque<comp::ChatMsg>,
|
||||||
new_notifications: VecDeque<Notification>,
|
new_notifications: VecDeque<Notification>,
|
||||||
@ -1772,9 +1824,9 @@ impl Hud {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.failed_block_pickups
|
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
|
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.
|
// Render overitem: name, etc.
|
||||||
for (entity, pos, item, distance) in (&entities, &pos, &items)
|
for (entity, pos, item, distance) in (&entities, &pos, &items)
|
||||||
@ -1793,7 +1845,7 @@ impl Hud {
|
|||||||
distance,
|
distance,
|
||||||
overitem::OveritemProperties {
|
overitem::OveritemProperties {
|
||||||
active: interactable.as_ref().and_then(|i| i.entity()) == Some(entity),
|
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,
|
&self.fonts,
|
||||||
vec![(GameInput::Interact, i18n.get("hud.pick_up").to_string())],
|
vec![(GameInput::Interact, i18n.get("hud.pick_up").to_string())],
|
||||||
@ -1810,7 +1862,7 @@ impl Hud {
|
|||||||
|
|
||||||
let overitem_properties = overitem::OveritemProperties {
|
let overitem_properties = overitem::OveritemProperties {
|
||||||
active: true,
|
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 pos = pos.map(|e| e as f32 + 0.5);
|
||||||
let over_pos = pos + Vec3::unit_z() * 0.7;
|
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>) {
|
pub fn add_failed_block_pickup(&mut self, pos: Vec3<i32>, reason: HudCollectFailedReason) {
|
||||||
self.failed_block_pickups.insert(pos, self.pulse);
|
self.failed_block_pickups
|
||||||
|
.insert(pos, CollectFailedData::new(self.pulse, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity) {
|
pub fn add_failed_entity_pickup(&mut self, entity: EcsEntity, reason: HudCollectFailedReason) {
|
||||||
self.failed_entity_pickups.insert(entity, self.pulse);
|
self.failed_entity_pickups
|
||||||
|
.insert(entity, CollectFailedData::new(self.pulse, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_loot_message(&mut self, item: LootMessage) {
|
pub fn new_loot_message(&mut self, item: LootMessage) {
|
||||||
|
@ -11,6 +11,7 @@ use conrod_core::{
|
|||||||
use i18n::Localization;
|
use i18n::Localization;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::hud::{CollectFailedData, HudCollectFailedReason, HudLootOwner};
|
||||||
use keyboard_keynames::key_layout::KeyLayout;
|
use keyboard_keynames::key_layout::KeyLayout;
|
||||||
|
|
||||||
pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0);
|
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 struct OveritemProperties {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub pickup_failed_pulse: Option<f32>,
|
pub pickup_failed_pulse: Option<CollectFailedData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
@ -215,9 +216,10 @@ impl<'a> Widget for Overitem<'a> {
|
|||||||
.parent(id)
|
.parent(id)
|
||||||
.set(state.ids.btn_bg, ui);
|
.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
|
//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 alpha = 1.0 - age.powi(4);
|
||||||
let brightness = 1.0 / (age / 0.07 - 1.0).abs().clamp(0.01, 1.0);
|
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)
|
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_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(inv_full_font_size as u32)
|
.font_size(inv_full_font_size as u32)
|
||||||
.color(shade_color(Color::Rgba(0.0, 0.0, 0.0, 1.0)))
|
.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)
|
.depth(self.distance_from_player_sqr + 6.0)
|
||||||
.set(state.ids.inv_full_bg, ui);
|
.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_id(self.fonts.cyri.conrod_id)
|
||||||
.font_size(inv_full_font_size as u32)
|
.font_size(inv_full_font_size as u32)
|
||||||
.color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0)))
|
.color(shade_color(Color::Rgba(1.0, 0.0, 0.0, 1.0)))
|
||||||
|
@ -43,7 +43,10 @@ use crate::{
|
|||||||
audio::sfx::SfxEvent,
|
audio::sfx::SfxEvent,
|
||||||
error::Error,
|
error::Error,
|
||||||
game_input::GameInput,
|
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,
|
key_state::KeyState,
|
||||||
menu::char_selection::CharSelectionState,
|
menu::char_selection::CharSelectionState,
|
||||||
render::{Drawer, GlobalsBindGroup},
|
render::{Drawer, GlobalsBindGroup},
|
||||||
@ -244,12 +247,27 @@ impl SessionState {
|
|||||||
global_state.audio.emit_sfx_item(sfx_trigger_item);
|
global_state.audio.emit_sfx_item(sfx_trigger_item);
|
||||||
|
|
||||||
match inv_event {
|
match inv_event {
|
||||||
InventoryUpdateEvent::BlockCollectFailed(pos) => {
|
InventoryUpdateEvent::BlockCollectFailed { pos, reason } => {
|
||||||
self.hud.add_failed_block_pickup(pos);
|
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()) {
|
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) => {
|
InventoryUpdateEvent::Collected(item) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user