Merge branch 'group-owned-loot' into 'master'

Implement group owned loot

See merge request veloren/veloren!3421
This commit is contained in:
Ben Wallis 2022-06-04 17:16:12 +00:00
commit 93a565e51b
10 changed files with 117 additions and 56 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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)]

View File

@ -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),
}

View File

@ -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

View File

@ -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(),
},
});

View File

@ -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 {

View File

@ -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>>();

View File

@ -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,

View File

@ -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()
},