Merge branch 'juliancoffee/handle_pvp' into 'master'

Handle PvP/PvE

See merge request veloren/veloren!2686
This commit is contained in:
Joshua Barretto 2021-08-03 21:09:09 +00:00
commit f4e04ecefc
15 changed files with 568 additions and 273 deletions

View File

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Healing sceptre crafting recipe
- NPCs can now warn players before engaging in combat
- Custom error message when a supported graphics backend can not be found
- Add server setting with PvE/PvP switch
### Changed

View File

@ -26,7 +26,8 @@ enum-iterator = "0.6"
vek = { version = "=0.14.1", features = ["serde"] }
# Strum
strum = "0.21"
strum = { version = "0.21", features = ["derive"] }
# TODO: remove this and rewrite every use of strum_macros to strum
strum_macros = "0.21"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@ -13,7 +13,7 @@ use crate::{
poise::PoiseChange,
skills::SkillGroupKind,
Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange,
HealthSource, Inventory, Ori, SkillSet, Stats,
HealthSource, Inventory, Ori, Player, SkillSet, Stats,
},
event::ServerEvent,
outcome::Outcome,
@ -71,6 +71,13 @@ pub struct TargetInfo<'a> {
pub char_state: Option<&'a CharacterState>,
}
#[derive(Clone, Copy)]
pub struct AttackOptions {
pub target_dodging: bool,
pub may_harm: bool,
pub target_group: GroupTarget,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive
pub struct Attack {
@ -161,25 +168,44 @@ impl Attack {
#[allow(clippy::too_many_arguments)]
pub fn apply_attack(
&self,
target_group: GroupTarget,
attacker: Option<AttackerInfo>,
target: TargetInfo,
dir: Dir,
target_dodging: bool,
// Currently just modifies damage, maybe look into modifying strength of other effects?
options: AttackOptions,
// Currently strength_modifier just modifies damage,
// maybe look into modifying strength of other effects?
strength_modifier: f32,
attack_source: AttackSource,
mut emit: impl FnMut(ServerEvent),
mut emit_outcome: impl FnMut(Outcome),
) -> bool {
let mut is_applied = false;
let AttackOptions {
target_dodging,
may_harm,
target_group,
} = options;
// target == OutOfGroup is basic heuristic that this
// "attack" has negative effects.
//
// so if target dodges this "attack" or we don't want to harm target,
// it should avoid such "damage" or effect
let avoid_damage = |attack_damage: &AttackDamage| {
matches!(attack_damage.target, Some(GroupTarget::OutOfGroup))
&& (target_dodging || !may_harm)
};
let avoid_effect = |attack_effect: &AttackEffect| {
matches!(attack_effect.target, Some(GroupTarget::OutOfGroup))
&& (target_dodging || !may_harm)
};
let is_crit = thread_rng().gen::<f32>() < self.crit_chance;
let mut is_applied = false;
let mut accumulated_damage = 0.0;
for damage in self
.damages
.iter()
.filter(|d| d.target.map_or(true, |t| t == target_group))
.filter(|d| !(matches!(d.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
.filter(|d| !avoid_damage(d))
{
is_applied = true;
let damage_reduction = Attack::compute_damage_reduction(
@ -294,7 +320,7 @@ impl Attack {
.effects
.iter()
.filter(|e| e.target.map_or(true, |t| t == target_group))
.filter(|e| !(matches!(e.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
.filter(|e| !avoid_effect(e))
{
if effect.requirements.iter().all(|req| match req {
CombatRequirement::AnyDamage => accumulated_damage > 0.0 && target.health.is_some(),
@ -430,6 +456,17 @@ impl Attack {
}
}
/// Checks if we should allow negative effects from one player to another
// FIXME: handle pets?
// This code works only with players.
// You still can kill someone's pet and
// you still can be killed by someone's pet
pub fn may_harm(attacker: Option<&Player>, target: Option<&Player>) -> bool {
attacker
.zip(target)
.map_or(true, |(attacker, target)| attacker.may_harm(target))
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AttackDamage {

View File

@ -81,22 +81,22 @@ impl BuffKind {
/// Checks if buff is buff or debuff
pub fn is_buff(self) -> bool {
match self {
BuffKind::Regeneration => true,
BuffKind::Saturation => true,
BuffKind::Bleeding => false,
BuffKind::Cursed => false,
BuffKind::Potion => true,
BuffKind::CampfireHeal => true,
BuffKind::IncreaseMaxEnergy => true,
BuffKind::IncreaseMaxHealth => true,
BuffKind::Invulnerability => true,
BuffKind::ProtectingWard => true,
BuffKind::Burning => false,
BuffKind::Crippled => false,
BuffKind::Frenzied => true,
BuffKind::Frozen => false,
BuffKind::Wet => false,
BuffKind::Ensnared => false,
BuffKind::Regeneration
| BuffKind::Saturation
| BuffKind::Potion
| BuffKind::CampfireHeal
| BuffKind::IncreaseMaxEnergy
| BuffKind::IncreaseMaxHealth
| BuffKind::Invulnerability
| BuffKind::ProtectingWard => true,
BuffKind::Bleeding
| BuffKind::Cursed
| BuffKind::Burning
| BuffKind::Crippled
| BuffKind::Frenzied
| BuffKind::Frozen
| BuffKind::Wet
| BuffKind::Ensnared => false,
}
}

View File

@ -3,6 +3,8 @@ use specs::{Component, DerefFlaggedStorage, NullStorage};
use specs_idvs::IdvStorage;
use uuid::Uuid;
use crate::resources::BattleMode;
const MAX_ALIAS_LEN: usize = 32;
#[derive(Debug)]
@ -17,11 +19,31 @@ pub enum DisconnectReason {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Player {
pub alias: String,
pub battle_mode: BattleMode,
uuid: Uuid,
}
impl BattleMode {
pub fn may_harm(self, other: Self) -> bool {
matches!((self, other), (BattleMode::PvP, BattleMode::PvP))
}
}
impl Player {
pub fn new(alias: String, uuid: Uuid) -> Self { Self { alias, uuid } }
pub fn new(alias: String, battle_mode: BattleMode, uuid: Uuid) -> Self {
Self {
alias,
battle_mode,
uuid,
}
}
/// Currently we allow attacking only if both players are opt-in to PvP.
///
/// Simple as tea, if they don't want the tea, don't make them drink the
/// tea.
/// You can make tea for yourself though.
pub fn may_harm(&self, other: &Player) -> bool { self.battle_mode.may_harm(other.battle_mode) }
pub fn is_valid(&self) -> bool { Self::alias_validate(&self.alias).is_ok() }

View File

@ -28,6 +28,15 @@ impl Effect {
}
}
pub fn is_harm(&self) -> bool {
match self {
Effect::Health(c) => c.amount < 0,
Effect::PoiseChange(c) => c.amount < 0,
Effect::Damage(_) => true,
Effect::Buff(e) => !e.kind.is_buff(),
}
}
pub fn modify_strength(&mut self, modifier: f32) {
match self {
Effect::Health(change) => {

View File

@ -74,3 +74,13 @@ impl PlayerPhysicsSetting {
pub struct PlayerPhysicsSettings {
pub settings: hashbrown::HashMap<uuid::Uuid, PlayerPhysicsSetting>,
}
/// Describe how players interact with other players.
///
/// May be removed when we will discover better way
/// to handle duels and murders
#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
pub enum BattleMode {
PvP,
PvE,
}

View File

@ -1,24 +1,26 @@
use common::{
combat,
comp::{
aura::{AuraChange, AuraKey, AuraKind, AuraTarget},
buff::{self, BuffCategory},
buff::{Buff, BuffCategory, BuffChange, BuffSource},
group::Group,
Auras, BuffKind, Buffs, CharacterState, Health, Pos,
Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos,
},
event::{EventBus, ServerEvent},
event::{Emitter, EventBus, ServerEvent},
resources::DeltaTime,
uid::{Uid, UidAllocator},
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, WriteStorage,
saveload::MarkerAllocator, shred::ResourceId, Entities, Entity as EcsEntity, Join, Read,
ReadStorage, SystemData, World, WriteStorage,
};
use std::time::Duration;
#[derive(SystemData)]
pub struct ReadData<'a> {
entities: Entities<'a>,
players: ReadStorage<'a, Player>,
dt: Read<'a, DeltaTime>,
server_bus: Read<'a, EventBus<ServerEvent>>,
uid_allocator: Read<'a, UidAllocator>,
@ -101,91 +103,36 @@ impl<'a> System<'a> for Sys {
Some(buff) => buff,
None => return,
};
// Ensure entity is within the aura radius
if target_pos.0.distance_squared(pos.0) < aura.radius.powi(2) {
if let AuraTarget::GroupOf(uid) = aura.target {
let same_group = read_data
// Ensure the entity is in the group we want to target
let same_group = |uid: Uid| {
read_data
.uid_allocator
.retrieve_entity_internal(uid.into())
.and_then(|e| read_data.groups.get(e))
.map_or(false, |owner_group| {
Some(owner_group) == read_data.groups.get(target)
})
|| *target_uid == uid;
if !same_group {
return;
}
}
// TODO: When more aura kinds (besides Buff) are
// implemented, match on them here
match aura.aura_kind {
AuraKind::Buff {
kind,
data,
category,
source,
} => {
let apply_buff = match kind {
BuffKind::CampfireHeal => {
matches!(
read_data.char_states.get(target),
Some(CharacterState::Sit)
) && health.current() < health.maximum()
},
// Add other specific buff conditions here
_ => true,
|| *target_uid == uid
};
if apply_buff {
// Checks that target is not already receiving a buff from
// an aura, where
// the buff is of the same kind, and is of at least
// the same strength and of at least the same duration
// If no such buff is present, adds the buff
let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| {
buff.cat_ids.iter().any(|cat_id| {
matches!(cat_id, BuffCategory::FromAura(_))
}) && buff.kind == kind
&& buff.data.strength >= data.strength
&& buff.time.map_or(true, |dur| {
data.duration.map_or(false, |dur_2| dur >= dur_2)
})
});
if emit_buff {
use buff::*;
server_emitter.emit(ServerEvent::Buff {
entity: target,
buff_change: BuffChange::Add(Buff::new(
kind,
data,
vec![category, BuffCategory::FromAura(true)],
source,
)),
});
}
// Finds all buffs on target that are from an aura, are of
// the
// same buff kind, and are of at most the same strength
// For any such buffs, marks it as recently applied
for (_, buff) in
target_buffs.buffs.iter_mut().filter(|(_, buff)| {
buff.cat_ids.iter().any(|cat_id| {
matches!(cat_id, BuffCategory::FromAura(_))
}) && buff.kind == kind
&& buff.data.strength <= data.strength
})
{
if let Some(cat_id) =
buff.cat_ids.iter_mut().find(|cat_id| {
matches!(cat_id, BuffCategory::FromAura(false))
})
{
*cat_id = BuffCategory::FromAura(true);
}
}
}
},
let is_target = match aura.target {
AuraTarget::GroupOf(uid) => same_group(uid),
AuraTarget::NotGroupOf(uid) => !same_group(uid),
AuraTarget::All => true,
};
if is_target {
activate_aura(
aura,
target,
health,
&mut target_buffs,
&read_data,
&mut server_emitter,
);
}
}
});
@ -201,3 +148,111 @@ impl<'a> System<'a> for Sys {
buffs.set_event_emission(true);
}
}
#[warn(clippy::pedantic)]
//#[warn(clippy::nursery)]
fn activate_aura(
aura: &Aura,
target: EcsEntity,
health: &Health,
target_buffs: &mut Buffs,
read_data: &ReadData,
server_emitter: &mut Emitter<ServerEvent>,
) {
let should_activate = match aura.aura_kind {
AuraKind::Buff { kind, source, .. } => {
let conditions_held = match kind {
BuffKind::CampfireHeal => {
let target_state = read_data.char_states.get(target);
matches!(target_state, Some(CharacterState::Sit))
&& health.current() < health.maximum()
},
// Add other specific buff conditions here
_ => true,
};
// TODO: this check will disable friendly fire with PvE switch.
//
// Which means that you can't apply debuffs on you and your group
// even if it's intented mechanics.
//
// Not that we have this for now, but think about this
// when we will add this.
let may_harm = || {
let owner = match source {
BuffSource::Character { by } => {
read_data.uid_allocator.retrieve_entity_internal(by.into())
},
_ => None,
};
owner.map_or(true, |attacker| {
let attacker = read_data.players.get(attacker);
let target = read_data.players.get(target);
combat::may_harm(attacker, target)
})
};
conditions_held && (kind.is_buff() || may_harm())
},
};
if !should_activate {
return;
}
// TODO: When more aura kinds (besides Buff) are
// implemented, match on them here
match aura.aura_kind {
AuraKind::Buff {
kind,
data,
category,
source,
} => {
// Checks that target is not already receiving a buff
// from an aura, where the buff is of the same kind,
// and is of at least the same strength
// and of at least the same duration.
// If no such buff is present, adds the buff.
let emit_buff = !target_buffs.buffs.iter().any(|(_, buff)| {
buff.cat_ids
.iter()
.any(|cat_id| matches!(cat_id, BuffCategory::FromAura(_)))
&& buff.kind == kind
&& buff.data.strength >= data.strength
&& buff.time.map_or(true, |dur| {
data.duration.map_or(false, |dur_2| dur >= dur_2)
})
});
if emit_buff {
server_emitter.emit(ServerEvent::Buff {
entity: target,
buff_change: BuffChange::Add(Buff::new(
kind,
data,
vec![category, BuffCategory::FromAura(true)],
source,
)),
});
}
// Finds all buffs on target that are from an aura, are of
// the same buff kind, and are of at most the same strength.
// For any such buffs, marks it as recently applied.
for (_, buff) in target_buffs.buffs.iter_mut().filter(|(_, buff)| {
buff.cat_ids
.iter()
.any(|cat_id| matches!(cat_id, BuffCategory::FromAura(_)))
&& buff.kind == kind
&& buff.data.strength <= data.strength
}) {
if let Some(cat_id) = buff
.cat_ids
.iter_mut()
.find(|cat_id| matches!(cat_id, BuffCategory::FromAura(false)))
{
*cat_id = BuffCategory::FromAura(true);
}
}
},
}
}

View File

@ -1,9 +1,9 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
Beam, BeamSegment, Body, CharacterState, Combo, Energy, Group, Health, HealthSource,
Inventory, Ori, Pos, Scale, Stats,
Inventory, Ori, Player, Pos, Scale, Stats,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
@ -26,6 +26,7 @@ use vek::*;
#[derive(SystemData)]
pub struct ReadData<'a> {
entities: Entities<'a>,
players: ReadStorage<'a, Player>,
server_bus: Read<'a, EventBus<ServerEvent>>,
time: Read<'a, Time>,
dt: Read<'a, DeltaTime>,
@ -212,12 +213,23 @@ impl<'a> System<'a> for Sys {
char_state: read_data.character_states.get(target),
};
beam_segment.properties.attack.apply_attack(
let may_harm = combat::may_harm(
beam_owner.and_then(|owner| read_data.players.get(owner)),
read_data.players.get(target),
);
let attack_options = AttackOptions {
// No luck with dodging beams
target_dodging: false,
may_harm,
target_group,
};
beam_segment.properties.attack.apply_attack(
attacker_info,
target_info,
ori.look_dir(),
false,
attack_options,
1.0,
AttackSource::Beam,
|e| server_events.push(e),

View File

@ -1,9 +1,9 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Pos, Scale,
Stats,
Body, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Player, Pos,
Scale, Stats,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
@ -22,6 +22,7 @@ use vek::*;
pub struct ReadData<'a> {
time: Read<'a, Time>,
entities: Entities<'a>,
players: ReadStorage<'a, Player>,
uids: ReadStorage<'a, Uid>,
positions: ReadStorage<'a, Pos>,
orientations: ReadStorage<'a, Ori>,
@ -115,7 +116,7 @@ impl<'a> System<'a> for Sys {
let rad_b = body_b.radius() * scale_b;
// Check if entity is dodging
let is_dodge = read_data
let target_dodging = read_data
.char_states
.get(target)
.map_or(false, |c_s| c_s.is_melee_dodge());
@ -161,12 +162,22 @@ impl<'a> System<'a> for Sys {
char_state: read_data.char_states.get(target),
};
let is_applied = melee_attack.attack.apply_attack(
let may_harm = combat::may_harm(
read_data.players.get(attacker),
read_data.players.get(target),
);
let attack_options = AttackOptions {
target_dodging,
may_harm,
target_group,
};
let is_applied = melee_attack.attack.apply_attack(
attacker_info,
target_info,
dir,
is_dodge,
attack_options,
1.0,
AttackSource::Melee,
|e| server_emitter.emit(e),

View File

@ -1,11 +1,11 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
projectile, Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory,
Ori, PhysicsState, Pos, Projectile, Stats, Vel,
Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel,
},
event::{EventBus, ServerEvent},
event::{Emitter, EventBus, ServerEvent},
outcome::Outcome,
resources::{DeltaTime, Time},
uid::{Uid, UidAllocator},
@ -15,8 +15,8 @@ use common::{
use common_ecs::{Job, Origin, Phase, System};
use rand::{thread_rng, Rng};
use specs::{
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, Write, WriteStorage,
saveload::MarkerAllocator, shred::ResourceId, Entities, Entity as EcsEntity, Join, Read,
ReadStorage, SystemData, World, Write, WriteStorage,
};
use std::time::Duration;
use vek::*;
@ -25,6 +25,7 @@ use vek::*;
pub struct ReadData<'a> {
time: Read<'a, Time>,
entities: Entities<'a>,
players: ReadStorage<'a, Player>,
dt: Read<'a, DeltaTime>,
uid_allocator: Read<'a, UidAllocator>,
server_bus: Read<'a, EventBus<ServerEvent>>,
@ -114,95 +115,37 @@ impl<'a> System<'a> for Sys {
}
let projectile = &mut *projectile;
let entity_of =
|uid: Uid| read_data.uid_allocator.retrieve_entity_internal(uid.into());
for effect in projectile.hit_entity.drain(..) {
match effect {
projectile::Effect::Attack(attack) => {
if let Some(target) = read_data
.uid_allocator
.retrieve_entity_internal(other.into())
{
if let (Some(pos), Some(ori)) =
(read_data.positions.get(target), orientations.get(entity))
{
let dir = ori.look_dir();
let owner_entity = projectile.owner.and_then(|u| {
read_data.uid_allocator.retrieve_entity_internal(u.into())
});
let attacker_info =
owner_entity.zip(projectile.owner).map(|(entity, uid)| {
AttackerInfo {
let owner = projectile.owner.and_then(entity_of);
let projectile_info = ProjectileInfo {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity),
}
});
let target_info = TargetInfo {
entity: target,
uid: other,
inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: pos.0,
ori: orientations.get(target),
char_state: read_data.character_states.get(target),
effect,
owner_uid: projectile.owner,
owner,
ori: orientations.get(entity),
pos,
};
if let Some(&body) = read_data.bodies.get(entity) {
outcomes.push(Outcome::ProjectileHit {
pos: pos.0,
body,
vel: read_data
.velocities
.get(entity)
.map_or(Vec3::zero(), |v| v.0),
source: projectile.owner,
target: read_data.uids.get(target).copied(),
});
}
attack.apply_attack(
let target = entity_of(other);
let projectile_target_info = ProjectileTargetInfo {
uid: other,
entity: target,
target_group,
attacker_info,
target_info,
dir,
false,
1.0,
AttackSource::Projectile,
|e| server_emitter.emit(e),
|o| outcomes.push(o),
ori: target.and_then(|target| orientations.get(target)),
};
dispatch_hit(
projectile_info,
projectile_target_info,
&read_data,
&mut projectile_vanished,
&mut outcomes,
&mut server_emitter,
);
}
}
},
projectile::Effect::Explode(e) => {
server_emitter.emit(ServerEvent::Explosion {
pos: pos.0,
explosion: e,
owner: projectile.owner,
});
},
projectile::Effect::Vanish => {
server_emitter.emit(ServerEvent::Destroy {
entity,
cause: HealthSource::World,
});
projectile_vanished = true;
},
projectile::Effect::Possess => {
if other != projectile.owner.unwrap() {
if let Some(owner) = projectile.owner {
server_emitter.emit(ServerEvent::Possess(owner, other));
}
}
},
_ => {},
}
}
if projectile_vanished {
continue 'projectile_loop;
@ -253,3 +196,141 @@ impl<'a> System<'a> for Sys {
}
}
}
struct ProjectileInfo<'a> {
entity: EcsEntity,
effect: projectile::Effect,
owner_uid: Option<Uid>,
owner: Option<EcsEntity>,
ori: Option<&'a Ori>,
pos: &'a Pos,
}
struct ProjectileTargetInfo<'a> {
uid: Uid,
entity: Option<EcsEntity>,
target_group: GroupTarget,
ori: Option<&'a Ori>,
}
fn dispatch_hit(
projectile_info: ProjectileInfo,
projectile_target_info: ProjectileTargetInfo,
read_data: &ReadData,
projectile_vanished: &mut bool,
outcomes: &mut Vec<Outcome>,
server_emitter: &mut Emitter<ServerEvent>,
) {
match projectile_info.effect {
projectile::Effect::Attack(attack) => {
let target_uid = projectile_target_info.uid;
let target = if let Some(entity) = projectile_target_info.entity {
entity
} else {
return;
};
let (target_pos, projectile_dir) = {
let target_pos = read_data.positions.get(target);
let projectile_ori = projectile_info.ori;
match target_pos.zip(projectile_ori) {
Some((tgt_pos, proj_ori)) => {
let Pos(tgt_pos) = tgt_pos;
(*tgt_pos, proj_ori.look_dir())
},
None => return,
}
};
let owner = projectile_info.owner;
let projectile_entity = projectile_info.entity;
let attacker_info =
owner
.zip(projectile_info.owner_uid)
.map(|(entity, uid)| AttackerInfo {
entity,
uid,
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity),
});
let target_info = TargetInfo {
entity: target,
uid: target_uid,
inventory: read_data.inventories.get(target),
stats: read_data.stats.get(target),
health: read_data.healths.get(target),
pos: target_pos,
ori: projectile_target_info.ori,
char_state: read_data.character_states.get(target),
};
// TODO: Is it possible to have projectile without body??
if let Some(&body) = read_data.bodies.get(projectile_entity) {
outcomes.push(Outcome::ProjectileHit {
pos: target_pos,
body,
vel: read_data
.velocities
.get(projectile_entity)
.map_or(Vec3::zero(), |v| v.0),
source: projectile_info.owner_uid,
target: read_data.uids.get(target).copied(),
});
}
let may_harm = combat::may_harm(
owner.and_then(|owner| read_data.players.get(owner)),
read_data.players.get(target),
);
let attack_options = AttackOptions {
// They say witchers can dodge arrows,
// but we don't have witchers
target_dodging: false,
may_harm,
target_group: projectile_target_info.target_group,
};
attack.apply_attack(
attacker_info,
target_info,
projectile_dir,
attack_options,
1.0,
AttackSource::Projectile,
|e| server_emitter.emit(e),
|o| outcomes.push(o),
);
},
projectile::Effect::Explode(e) => {
let Pos(pos) = *projectile_info.pos;
let owner_uid = projectile_info.owner_uid;
server_emitter.emit(ServerEvent::Explosion {
pos,
explosion: e,
owner: owner_uid,
});
},
projectile::Effect::Vanish => {
let entity = projectile_info.entity;
server_emitter.emit(ServerEvent::Destroy {
entity,
cause: HealthSource::World,
});
*projectile_vanished = true;
},
projectile::Effect::Possess => {
let target_uid = projectile_target_info.uid;
let owner_uid = projectile_info.owner_uid;
if let Some(owner_uid) = owner_uid {
if target_uid != owner_uid {
server_emitter.emit(ServerEvent::Possess(owner_uid, target_uid));
}
}
},
projectile::Effect::Stick => {},
}
}

View File

@ -1,9 +1,9 @@
use common::{
combat::{AttackSource, AttackerInfo, TargetInfo},
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{
agent::{Sound, SoundKind},
Body, CharacterState, Combo, Energy, Group, Health, HealthSource, Inventory, Ori,
PhysicsState, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
},
event::{EventBus, ServerEvent},
outcome::Outcome,
@ -25,6 +25,7 @@ pub struct ReadData<'a> {
entities: Entities<'a>,
server_bus: Read<'a, EventBus<ServerEvent>>,
time: Read<'a, Time>,
players: ReadStorage<'a, Player>,
dt: Read<'a, DeltaTime>,
uid_allocator: Read<'a, UidAllocator>,
uids: ReadStorage<'a, Uid>,
@ -208,12 +209,22 @@ impl<'a> System<'a> for Sys {
char_state: read_data.character_states.get(target),
};
shockwave.properties.attack.apply_attack(
let may_harm = combat::may_harm(
shockwave_owner.and_then(|owner| read_data.players.get(owner)),
read_data.players.get(target),
);
let attack_options = AttackOptions {
// Trying roll during earthquake isn't the best idea
target_dodging: false,
may_harm,
target_group,
};
shockwave.properties.attack.apply_attack(
attacker_info,
target_info,
dir,
false,
attack_options,
1.0,
AttackSource::Shockwave,
|e| server_emitter.emit(e),

View File

@ -819,6 +819,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
let energies = &ecs.read_storage::<comp::Energy>();
let combos = &ecs.read_storage::<comp::Combo>();
let inventories = &ecs.read_storage::<comp::Inventory>();
let players = &ecs.read_storage::<comp::Player>();
for (
entity_b,
pos_b,
@ -887,12 +888,23 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
char_state: char_state_b_maybe,
};
attack.apply_attack(
let may_harm = combat::may_harm(
owner_entity.and_then(|owner| players.get(owner)),
players.get(entity_b),
);
let attack_options = combat::AttackOptions {
// cool guyz maybe don't look at explosions
// but they still got hurt, it's not Hollywood
target_dodging: false,
may_harm,
target_group,
};
attack.apply_attack(
attacker_info,
target_info,
dir,
false,
attack_options,
strength,
combat::AttackSource::Explosion,
|e| server_eventbus.emit_now(e),
@ -902,6 +914,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
}
},
RadiusEffect::Entity(mut effect) => {
let players = &ecs.read_storage::<comp::Player>();
for (entity_b, pos_b, body_b_maybe) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
@ -916,17 +929,38 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
1.0 - distance_squared / explosion.radius.powi(2)
};
// Player check only accounts for PvP/PvE flag.
//
// But bombs are intented to do
// friendly fire.
//
// What exactly friendly fire is subject to discussion.
// As we probably want to minimize possibility of being dick
// even to your group members, the only exception is when
// you want to harm yourself.
//
// This can be changed later.
let may_harm = || {
owner_entity.map_or(false, |attacker| {
let attacker_player = players.get(attacker);
let target_player = players.get(entity_b);
combat::may_harm(attacker_player, target_player) || attacker == entity_b
})
};
if strength > 0.0 {
let is_alive = ecs
.read_storage::<comp::Health>()
.get(entity_b)
.map_or(true, |h| !h.is_dead);
if is_alive {
effect.modify_strength(strength);
if !effect.is_harm() || may_harm() {
server.state().apply_effect(entity_b, effect.clone(), owner);
}
}
}
}
},
}
}

View File

@ -14,6 +14,7 @@ pub use server_description::ServerDescription;
pub use whitelist::{Whitelist, WhitelistInfo, WhitelistRecord};
use chrono::Utc;
use common::resources::BattleMode;
use core::time::Duration;
use portpicker::pick_unused_port;
use serde::{Deserialize, Serialize};
@ -48,7 +49,7 @@ pub struct Settings {
pub quic_files: Option<X509FilePair>,
pub max_players: usize,
pub world_seed: u32,
//pub pvp_enabled: bool,
pub battle_mode: BattleMode,
pub server_name: String,
pub start_time: f64,
/// When set to None, loads the default map file (if available); otherwise,
@ -73,6 +74,7 @@ impl Default for Settings {
world_seed: DEFAULT_WORLD_SEED,
server_name: "Veloren Alpha".into(),
max_players: 100,
battle_mode: BattleMode::PvP,
start_time: 9.0 * 3600.0,
map_file: None,
max_view_distance: Some(65),

View File

@ -2,7 +2,7 @@ use crate::{
client::Client,
login_provider::{LoginProvider, PendingLogin},
metrics::PlayerMetrics,
EditableSettings,
EditableSettings, Settings,
};
use common::{
comp::{Admin, Player, Stats},
@ -17,7 +17,8 @@ use common_net::msg::{
use hashbrown::HashMap;
use plugin_api::Health;
use specs::{
storage::StorageEntry, Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage,
shred::ResourceId, storage::StorageEntry, Entities, Join, Read, ReadExpect, ReadStorage,
SystemData, World, WriteExpect, WriteStorage,
};
use tracing::trace;
@ -29,26 +30,32 @@ type ReadPlugin<'a> = Read<'a, PluginMgr>;
#[cfg(not(feature = "plugins"))]
type ReadPlugin<'a> = Option<Read<'a, ()>>;
#[derive(SystemData)]
pub struct ReadData<'a> {
entities: Entities<'a>,
stats: ReadStorage<'a, Stats>,
uids: ReadStorage<'a, Uid>,
clients: ReadStorage<'a, Client>,
server_event_bus: Read<'a, EventBus<ServerEvent>>,
player_metrics: ReadExpect<'a, PlayerMetrics>,
settings: ReadExpect<'a, Settings>,
editable_settings: ReadExpect<'a, EditableSettings>,
_healths: ReadStorage<'a, Health>, // used by plugin feature
_plugin_mgr: ReadPlugin<'a>, // used by plugin feature
_uid_allocator: Read<'a, UidAllocator>, // used by plugin feature
}
/// This system will handle new messages from clients
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
#[allow(clippy::type_complexity)]
type SystemData = (
Entities<'a>,
ReadExpect<'a, PlayerMetrics>,
ReadStorage<'a, Health>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Client>,
ReadData<'a>,
WriteStorage<'a, Player>,
WriteStorage<'a, PendingLogin>,
Read<'a, UidAllocator>,
ReadPlugin<'a>,
ReadStorage<'a, Stats>,
WriteExpect<'a, LoginProvider>,
WriteStorage<'a, Admin>,
ReadExpect<'a, EditableSettings>,
Read<'a, EventBus<ServerEvent>>,
WriteStorage<'a, PendingLogin>,
WriteExpect<'a, LoginProvider>,
);
const NAME: &'static str = "msg::register";
@ -58,24 +65,21 @@ impl<'a> System<'a> for Sys {
fn run(
_job: &mut Job<Self>,
(
entities,
player_metrics,
_health_comp, // used by plugin feature
uids,
clients,
read_data,
mut players,
mut pending_logins,
_uid_allocator, // used by plugin feature
_plugin_mgr, // used by plugin feature
stats,
mut login_provider,
mut admins,
editable_settings,
server_event_bus,
mut pending_logins,
mut login_provider,
): Self::SystemData,
) {
let mut server_emitter = read_data.server_event_bus.emitter();
// Player list to send new players.
let player_list = (&uids, &players, stats.maybe(), admins.maybe())
let player_list = (
&read_data.uids,
&players,
read_data.stats.maybe(),
admins.maybe(),
)
.join()
.map(|(uid, player, stats, admin)| {
(*uid, PlayerInfo {
@ -92,7 +96,7 @@ impl<'a> System<'a> for Sys {
let mut new_players = Vec::new();
// defer auth lockup
for (entity, client) in (&entities, &clients).join() {
for (entity, client) in (&read_data.entities, &read_data.clients).join() {
let _ = super::try_recv_all(client, 0, |_, msg: ClientRegister| {
trace!(?msg.token_or_username, "defer auth lockup");
let pending = login_provider.verify(&msg.token_or_username);
@ -103,15 +107,17 @@ impl<'a> System<'a> for Sys {
let mut finished_pending = vec![];
let mut retries = vec![];
for (entity, client, mut pending) in (&entities, &clients, &mut pending_logins).join() {
for (entity, client, mut pending) in
(&read_data.entities, &read_data.clients, &mut pending_logins).join()
{
if let Err(e) = || -> std::result::Result<(), crate::error::Error> {
#[cfg(feature = "plugins")]
let ecs_world = EcsWorld {
entities: &entities,
health: (&_health_comp).into(),
uid: (&uids).into(),
entities: &read_data.entities,
health: (&read_data._healths).into(),
uid: (&read_data.uids).into(),
player: (&players).into(),
uid_allocator: &_uid_allocator,
uid_allocator: &read_data._uid_allocator,
};
let (username, uuid) = match login_provider.login(
@ -119,10 +125,10 @@ impl<'a> System<'a> for Sys {
#[cfg(feature = "plugins")]
&ecs_world,
#[cfg(feature = "plugins")]
&_plugin_mgr,
&*editable_settings.admins,
&*editable_settings.whitelist,
&*editable_settings.banlist,
&read_data._plugin_mgr,
&*read_data.editable_settings.admins,
&*read_data.editable_settings.whitelist,
&*read_data.editable_settings.banlist,
) {
None => return Ok(()),
Some(r) => {
@ -130,7 +136,7 @@ impl<'a> System<'a> for Sys {
trace!(?r, "pending login returned");
match r {
Err(e) => {
server_event_bus.emit_now(ServerEvent::ClientDisconnect(
server_emitter.emit(ServerEvent::ClientDisconnect(
entity,
common::comp::DisconnectReason::Kicked,
));
@ -143,12 +149,13 @@ impl<'a> System<'a> for Sys {
};
// Check if user is already logged-in
if let Some((old_entity, old_client, _)) = (&entities, &clients, &players)
if let Some((old_entity, old_client, _)) =
(&read_data.entities, &read_data.clients, &players)
.join()
.find(|(_, _, old_player)| old_player.uuid() == uuid)
{
// Remove old client
server_event_bus.emit_now(ServerEvent::ClientDisconnect(
server_emitter.emit(ServerEvent::ClientDisconnect(
old_entity,
common::comp::DisconnectReason::NewerLogin,
));
@ -166,8 +173,8 @@ impl<'a> System<'a> for Sys {
return Ok(());
}
let player = Player::new(username, uuid);
let admin = editable_settings.admins.get(&uuid);
let player = Player::new(username, read_data.settings.battle_mode, uuid);
let admin = read_data.editable_settings.admins.get(&uuid);
if !player.is_valid() {
// Invalid player
@ -178,7 +185,7 @@ impl<'a> System<'a> for Sys {
if let Ok(StorageEntry::Vacant(v)) = players.entry(entity) {
// Add Player component to this client, if the entity exists.
v.insert(player);
player_metrics.players_connected.inc();
read_data.player_metrics.players_connected.inc();
// Give the Admin component to the player if their name exists in
// admin list
@ -214,10 +221,13 @@ impl<'a> System<'a> for Sys {
// Handle new players.
// Tell all clients to add them to the player list.
for entity in new_players {
if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) {
let player_info = |entity| {
let player_info = read_data.uids.get(entity).zip(players.get(entity));
player_info.map(|(u, p)| (entity, u, p))
};
for (entity, uid, player) in new_players.into_iter().filter_map(player_info) {
let mut lazy_msg = None;
for (_, client) in (&players, &clients).join() {
for (_, client) in (&players, &read_data.clients).join() {
if lazy_msg.is_none() {
lazy_msg = Some(client.prepare(ServerGeneral::PlayerListUpdate(
PlayerListUpdate::Add(*uid, PlayerInfo {
@ -233,4 +243,3 @@ impl<'a> System<'a> for Sys {
}
}
}
}