mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'group-owned-loot' into 'master'
Implement group owned loot See merge request veloren/veloren!3421
This commit is contained in:
commit
93a565e51b
@ -9,7 +9,9 @@
|
||||
"hud.you_died": "Du bist gestorben",
|
||||
"hud.waypoint_saved": "Wegpunkt gespeichert",
|
||||
"hud.sp_arrow_txt": "SP",
|
||||
"hud.inventory_full": "Inventar voll",
|
||||
"hud.inventory_full": "Inventar voll",
|
||||
"hud.someone_else": "jemand anderem",
|
||||
"hud.another_group": "einer anderen Gruppe",
|
||||
|
||||
"hud.press_key_to_show_keybindings_fmt": "[{key}] Kurzwahltasten",
|
||||
"hud.press_key_to_toggle_lantern_fmt": "[{key}] Laterne",
|
||||
|
@ -11,6 +11,7 @@
|
||||
"hud.sp_arrow_txt": "SP",
|
||||
"hud.inventory_full": "Inventory Full",
|
||||
"hud.someone_else": "someone else",
|
||||
"hud.another_group": "another group",
|
||||
"hud.owned_by_for_secs": "Owned by {name} for {secs} secs",
|
||||
|
||||
"hud.press_key_to_show_keybindings_fmt": "[{key}] Keybindings",
|
||||
|
@ -14,6 +14,7 @@ use crate::{
|
||||
loadout::Loadout,
|
||||
slot::{EquipSlot, Slot, SlotError},
|
||||
},
|
||||
loot_owner::LootOwnerKind,
|
||||
slot::{InvSlotId, SlotId},
|
||||
Item,
|
||||
},
|
||||
@ -825,7 +826,10 @@ impl Component for Inventory {
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum CollectFailedReason {
|
||||
InventoryFull,
|
||||
LootOwned { owner_uid: Uid, expiry_secs: u64 },
|
||||
LootOwned {
|
||||
owner: LootOwnerKind,
|
||||
expiry_secs: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
comp::{Alignment, Body, Player},
|
||||
comp::{Alignment, Body, Group, Player},
|
||||
uid::Uid,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -15,21 +15,28 @@ 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,
|
||||
owner: LootOwnerKind,
|
||||
}
|
||||
|
||||
// Loot becomes free-for-all after the initial ownership period
|
||||
const OWNERSHIP_SECS: u64 = 45;
|
||||
|
||||
impl LootOwner {
|
||||
pub fn new(uid: Uid) -> Self {
|
||||
pub fn new(kind: LootOwnerKind) -> Self {
|
||||
Self {
|
||||
expiry: Instant::now().add(Duration::from_secs(OWNERSHIP_SECS)),
|
||||
owner_uid: uid,
|
||||
owner: kind,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid(&self) -> Uid { self.owner_uid }
|
||||
pub fn uid(&self) -> Option<Uid> {
|
||||
match &self.owner {
|
||||
LootOwnerKind::Player(uid) => Some(*uid),
|
||||
LootOwnerKind::Group(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> LootOwnerKind { self.owner }
|
||||
|
||||
pub fn time_until_expiration(&self) -> Duration { self.expiry - Instant::now() }
|
||||
|
||||
@ -40,6 +47,7 @@ impl LootOwner {
|
||||
pub fn can_pickup(
|
||||
&self,
|
||||
uid: Uid,
|
||||
group: Option<&Group>,
|
||||
alignment: Option<&Alignment>,
|
||||
body: Option<&Body>,
|
||||
player: Option<&Player>,
|
||||
@ -48,7 +56,12 @@ impl LootOwner {
|
||||
let is_player = player.is_some();
|
||||
let is_pet = is_owned && !is_player;
|
||||
|
||||
let owns_loot = self.uid().0 == uid.0;
|
||||
let owns_loot = match self.owner {
|
||||
LootOwnerKind::Player(loot_uid) => loot_uid.0 == uid.0,
|
||||
LootOwnerKind::Group(loot_group) => {
|
||||
matches!(group, Some(group) if loot_group == *group)
|
||||
},
|
||||
};
|
||||
let is_humanoid = matches!(body, Some(Body::Humanoid(_)));
|
||||
|
||||
// Pet's can't pick up owned loot
|
||||
@ -61,3 +74,9 @@ impl LootOwner {
|
||||
impl Component for LootOwner {
|
||||
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum LootOwnerKind {
|
||||
Player(Uid),
|
||||
Group(Group),
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ use common::{
|
||||
self, aura, buff,
|
||||
chat::{KillSource, KillType},
|
||||
inventory::item::MaterialStatManifest,
|
||||
loot_owner::LootOwnerKind,
|
||||
Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory,
|
||||
Player, Poise, Pos, SkillSet, Stats,
|
||||
},
|
||||
@ -205,7 +206,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
}
|
||||
}
|
||||
|
||||
let mut exp_awards = Vec::<(Entity, f32)>::new();
|
||||
let mut exp_awards = Vec::<(Entity, f32, Option<Group>)>::new();
|
||||
// Award EXP to damage contributors
|
||||
//
|
||||
// NOTE: Debug logging is disabled by default for this module - to enable it add
|
||||
@ -327,7 +328,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
positions.get(*attacker).and_then(|attacker_pos| {
|
||||
if within_range(attacker_pos) {
|
||||
debug!("Awarding {} exp to individual {:?} who contributed {}% damage to the kill of {:?}", contributor_exp, attacker, *damage_percent * 100.0, entity);
|
||||
Some(iter::once((*attacker, contributor_exp)).collect())
|
||||
Some(iter::once((*attacker, contributor_exp, None)).collect())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -361,16 +362,16 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
let exp_per_member = contributor_exp / (members_in_range.len() as f32).sqrt();
|
||||
|
||||
debug!("Awarding {} exp per member of group ID {:?} with {} members which contributed {}% damage to the kill of {:?}", exp_per_member, group, members_in_range.len(), *damage_percent * 100.0, entity);
|
||||
Some(members_in_range.into_iter().map(|entity| (entity, exp_per_member)).collect::<Vec<(Entity, f32)>>())
|
||||
Some(members_in_range.into_iter().map(|entity| (entity, exp_per_member, Some(*group))).collect::<Vec<(Entity, f32, Option<Group>)>>())
|
||||
},
|
||||
DamageContrib::NotFound => {
|
||||
// Discard exp for dead/offline individual damage contributors
|
||||
None
|
||||
}
|
||||
}
|
||||
}).flatten().collect::<Vec<(Entity, f32)>>();
|
||||
}).flatten().collect::<Vec<(Entity, f32, Option<Group>)>>();
|
||||
|
||||
exp_awards.iter().for_each(|(attacker, exp_reward)| {
|
||||
exp_awards.iter().for_each(|(attacker, exp_reward, _)| {
|
||||
// Process the calculated EXP rewards
|
||||
if let (
|
||||
Some(mut attacker_skill_set),
|
||||
@ -447,11 +448,11 @@ 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: Figure out how damage contributions of 0% are being awarded - for now
|
||||
// just remove them to avoid a crash when creating the WeightedIndex
|
||||
let _ = exp_awards.drain_filter(|(_, exp)| *exp < f32::EPSILON);
|
||||
// Remove entries where zero exp was awarded - this happens because some
|
||||
// entities like Object bodies don't give EXP.
|
||||
let _ = exp_awards.drain_filter(|(_, exp, _)| *exp < f32::EPSILON);
|
||||
|
||||
let winner_uid = if exp_awards.is_empty() {
|
||||
let winner = if exp_awards.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// Use the awarded exp per entity as the weight distribution for drop chance
|
||||
@ -462,23 +463,30 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
let mut rng = rand::thread_rng();
|
||||
let winner = exp_awards
|
||||
.get(dist.sample(&mut rng))
|
||||
.expect("Loot distribution failed to find a winner")
|
||||
.0;
|
||||
.expect("Loot distribution failed to find a winner");
|
||||
let (winner, group) = (winner.0, winner.2);
|
||||
|
||||
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()
|
||||
if let Some(group) = group {
|
||||
Some(LootOwnerKind::Group(group))
|
||||
} else {
|
||||
let uid = 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();
|
||||
|
||||
uid.map(LootOwnerKind::Player)
|
||||
}
|
||||
};
|
||||
|
||||
let item_drop_entity = state
|
||||
@ -489,7 +497,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
|
||||
// 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 {
|
||||
if let Some(uid) = winner {
|
||||
debug!("Assigned UID {:?} as the winner for the loot drop", uid);
|
||||
|
||||
state
|
||||
|
@ -146,8 +146,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
let alignments = state.ecs().read_storage::<Alignment>();
|
||||
let bodies = state.ecs().read_storage::<Body>();
|
||||
let players = state.ecs().read_storage::<Player>();
|
||||
let groups = state.ecs().read_storage::<Group>();
|
||||
let can_pickup = loot_owner.can_pickup(
|
||||
uid,
|
||||
groups.get(entity),
|
||||
alignments.get(entity),
|
||||
bodies.get(entity),
|
||||
players.get(entity),
|
||||
@ -157,7 +159,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
|
||||
comp::InventoryUpdate::new(InventoryUpdateEvent::EntityCollectFailed {
|
||||
entity: pickup_uid,
|
||||
reason: CollectFailedReason::LootOwned {
|
||||
owner_uid: loot_owner.uid(),
|
||||
owner: loot_owner.owner(),
|
||||
expiry_secs: loot_owner.time_until_expiration().as_secs(),
|
||||
},
|
||||
});
|
||||
|
@ -1620,7 +1620,13 @@ impl<'a> AgentData<'a> {
|
||||
.loot_owners
|
||||
.get(entity)
|
||||
.map_or(true, |loot_owner| {
|
||||
loot_owner.can_pickup(*self.uid, self.alignment, self.body, None)
|
||||
loot_owner.can_pickup(
|
||||
*self.uid,
|
||||
read_data.groups.get(entity),
|
||||
self.alignment,
|
||||
self.body,
|
||||
None,
|
||||
)
|
||||
});
|
||||
|
||||
if attempt_pickup {
|
||||
|
@ -1,4 +1,7 @@
|
||||
use common::{comp::LootOwner, uid::UidAllocator};
|
||||
use common::{
|
||||
comp::{group::GroupManager, loot_owner::LootOwnerKind, LootOwner},
|
||||
uid::UidAllocator,
|
||||
};
|
||||
use common_ecs::{Job, Origin, Phase, System};
|
||||
use specs::{saveload::MarkerAllocator, Entities, Entity, Join, Read, WriteStorage};
|
||||
use tracing::debug;
|
||||
@ -11,22 +14,29 @@ impl<'a> System<'a> for Sys {
|
||||
Entities<'a>,
|
||||
WriteStorage<'a, LootOwner>,
|
||||
Read<'a, UidAllocator>,
|
||||
Read<'a, GroupManager>,
|
||||
);
|
||||
|
||||
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) {
|
||||
fn run(
|
||||
_job: &mut Job<Self>,
|
||||
(entities, mut loot_owners, uid_allocator, group_manager): 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
|
||||
// the expiry time has passed, or the owner 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))
|
||||
|| match loot_owner.owner() {
|
||||
LootOwnerKind::Player(uid) => uid_allocator
|
||||
.retrieve_entity_internal(uid.into())
|
||||
.map_or(true, |entity| !entities.is_alive(entity)),
|
||||
LootOwnerKind::Group(group) => group_manager.group_info(group).is_none(),
|
||||
}
|
||||
})
|
||||
.map(|(entity, _)| entity)
|
||||
.collect::<Vec<Entity>>();
|
||||
|
@ -82,6 +82,7 @@ use common::{
|
||||
fluid_dynamics,
|
||||
inventory::{slot::InvSlotId, trade_pricing::TradePricing, CollectFailedReason},
|
||||
item::{tool::ToolKind, ItemDesc, MaterialStatManifest, Quality},
|
||||
loot_owner::LootOwnerKind,
|
||||
pet::is_mountable,
|
||||
skillset::{skills::Skill, SkillGroupKind},
|
||||
BuffData, BuffKind, Item, MapMarkerChange,
|
||||
@ -1000,6 +1001,7 @@ pub struct Floaters {
|
||||
#[derive(Clone)]
|
||||
pub enum HudLootOwner {
|
||||
Name(String),
|
||||
Group,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
@ -1016,21 +1018,25 @@ 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
|
||||
CollectFailedReason::LootOwned { owner, expiry_secs } => {
|
||||
let owner = match owner {
|
||||
LootOwnerKind::Player(owner_uid) => {
|
||||
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())
|
||||
});
|
||||
|
||||
if let Some(name) = maybe_owner_name {
|
||||
HudLootOwner::Name(name)
|
||||
} else {
|
||||
HudLootOwner::Unknown
|
||||
}
|
||||
},
|
||||
LootOwnerKind::Group(_) => HudLootOwner::Group,
|
||||
};
|
||||
|
||||
HudCollectFailedReason::LootOwned {
|
||||
owner,
|
||||
expiry_secs: *expiry_secs,
|
||||
|
@ -235,6 +235,9 @@ impl<'a> Widget for Overitem<'a> {
|
||||
HudCollectFailedReason::LootOwned { owner, expiry_secs } => {
|
||||
let owner_name = match owner {
|
||||
HudLootOwner::Name(name) => name,
|
||||
HudLootOwner::Group => {
|
||||
self.localized_strings.get("hud.another_group").to_string()
|
||||
},
|
||||
HudLootOwner::Unknown => {
|
||||
self.localized_strings.get("hud.someone_else").to_string()
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user