diff --git a/common/src/combat.rs b/common/src/combat.rs index 1bd42c2389..a7c923624b 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -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 target_group: GroupTarget, + pub avoid_harm: bool, +} + #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive pub struct Attack { @@ -158,28 +165,47 @@ impl Attack { 1.0 - (1.0 - damage_reduction) * (1.0 - block_reduction) } + #[allow(clippy::too_many_arguments)] pub fn apply_attack( &self, - target_group: GroupTarget, attacker: Option, 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, + target_group, + avoid_harm, + } = 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 || avoid_harm) + }; + let avoid_effect = |attack_effect: &AttackEffect| { + matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)) + && (target_dodging || avoid_harm) + }; let is_crit = thread_rng().gen::() < 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(), diff --git a/common/src/comp/player.rs b/common/src/comp/player.rs index 7a34664569..a9f7ce59bd 100644 --- a/common/src/comp/player.rs +++ b/common/src/comp/player.rs @@ -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,34 @@ pub enum DisconnectReason { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Player { pub alias: String, + pub battle_mode: BattleMode, uuid: Uuid, } 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. + pub fn allow_harm(&self, other: &Player) -> bool { + // TODO: discuss if we want to keep self-harm + matches!( + (self.battle_mode, other.battle_mode), + (BattleMode::PvP, BattleMode::PvP) + ) + } + + /// Inverse of `allow_harm`. Read its doc to learn more. + pub fn disallow_harm(&self, other: &Player) -> bool { + !self.allow_harm(other) + } pub fn is_valid(&self) -> bool { Self::alias_validate(&self.alias).is_ok() } diff --git a/common/src/effect.rs b/common/src/effect.rs index aca9cf0ca8..a1b6b599e3 100644 --- a/common/src/effect.rs +++ b/common/src/effect.rs @@ -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) => { diff --git a/common/src/resources.rs b/common/src/resources.rs index e6d798ce84..ee57b3242a 100644 --- a/common/src/resources.rs +++ b/common/src/resources.rs @@ -74,3 +74,13 @@ impl PlayerPhysicsSetting { pub struct PlayerPhysicsSettings { pub settings: hashbrown::HashMap, } + +/// Describe how players interact with other players. +/// +/// Probably will be removed when we will discover better way +/// to handle duels and murders +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub enum BattleMode { + PvP, + PvE, +} diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index dae48525d2..0a3a86e93a 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -1,9 +1,9 @@ use common::{ - combat::{AttackSource, AttackerInfo, TargetInfo}, + combat::{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>, time: Read<'a, Time>, dt: Read<'a, DeltaTime>, @@ -212,12 +213,32 @@ impl<'a> System<'a> for Sys { char_state: read_data.character_states.get(target), }; - beam_segment.properties.attack.apply_attack( + // No luck with dodging beams + let is_dodge = false; + let avoid_harm = { + let players = &read_data.players; + beam_owner.map_or(false, |attacker| { + if let (Some(attacker), Some(target)) = + (players.get(attacker), players.get(target)) + { + attacker.disallow_harm(target) + } else { + false + } + }) + }; + + let attack_options = AttackOptions { + target_dodging: is_dodge, target_group, + avoid_harm, + }; + + 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), diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index 20ac8b54a9..5844b3711d 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -1,9 +1,9 @@ use common::{ - combat::{AttackSource, AttackerInfo, TargetInfo}, + combat::{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>, @@ -161,12 +162,40 @@ impl<'a> System<'a> for Sys { char_state: read_data.char_states.get(target), }; - let is_applied = melee_attack.attack.apply_attack( + let avoid_harm = { + let players = &read_data.players; + if let (Some(attacker), Some(target)) = + (players.get(attacker), players.get(target)) + { + attacker.disallow_harm(target) + } else { + false + } + }; + + // FIXME: printf debugging, this shouldn't go to master + if let Some(attacker) = read_data.players.get(attacker) { + println!("attacker battle_mode: {:?}", attacker.battle_mode); + } else { + println!("attacker special casing") + } + if let Some(target) = read_data.players.get(target) { + println!("target battle_mode: {:?}", target.battle_mode); + } else { + println!("target special casing") + } + + let attack_options = AttackOptions { + target_dodging: is_dodge, target_group, + avoid_harm, + }; + + 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), diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index e9d7a5a3e8..73dbbea93e 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -1,9 +1,9 @@ use common::{ - combat::{AttackSource, AttackerInfo, TargetInfo}, + combat::{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}, outcome::Outcome, @@ -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>, @@ -152,6 +153,22 @@ impl<'a> System<'a> for Sys { char_state: read_data.character_states.get(target), }; + // They say witchers can dodge arrows, + // but we don't have witchers + let is_dodge = false; + let avoid_harm = { + let players = &read_data.players; + projectile_owner.map_or(false, |attacker| { + if let (Some(attacker), Some(target)) = + (players.get(attacker), players.get(target)) + { + attacker.disallow_harm(target) + } else { + false + } + }) + }; + if let Some(&body) = read_data.bodies.get(entity) { outcomes.push(Outcome::ProjectileHit { pos: pos.0, @@ -165,12 +182,16 @@ impl<'a> System<'a> for Sys { }); } - attack.apply_attack( + let attack_options = AttackOptions { + target_dodging: is_dodge, target_group, + avoid_harm, + }; + attack.apply_attack( attacker_info, target_info, dir, - false, + attack_options, 1.0, AttackSource::Projectile, |e| server_emitter.emit(e), diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index 2997821811..fbfe31756f 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -1,9 +1,9 @@ use common::{ - combat::{AttackSource, AttackerInfo, TargetInfo}, + combat::{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>, time: Read<'a, Time>, + players: ReadStorage<'a, Player>, dt: Read<'a, DeltaTime>, uid_allocator: Read<'a, UidAllocator>, uids: ReadStorage<'a, Uid>, @@ -208,12 +209,32 @@ impl<'a> System<'a> for Sys { char_state: read_data.character_states.get(target), }; - shockwave.properties.attack.apply_attack( + // Trying roll during earthquake isn't the best idea + let is_dodge = false; + let avoid_harm = { + let players = &read_data.players; + shockwave_owner.map_or(false, |attacker| { + if let (Some(attacker), Some(target)) = + (players.get(attacker), players.get(target)) + { + attacker.disallow_harm(target) + } else { + false + } + }) + }; + + let attack_options = AttackOptions { + target_dodging: is_dodge, target_group, + avoid_harm, + }; + + shockwave.properties.attack.apply_attack( attacker_info, target_info, dir, - false, + attack_options, 1.0, AttackSource::Shockwave, |e| server_emitter.emit(e), diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 345984e2a2..da92740aba 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -819,6 +819,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o let energies = &ecs.read_storage::(); let combos = &ecs.read_storage::(); let inventories = &ecs.read_storage::(); + let players = &ecs.read_storage::(); for ( entity_b, pos_b, @@ -887,12 +888,31 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o char_state: char_state_b_maybe, }; - attack.apply_attack( + let avoid_harm = { + owner_entity.map_or(false, |attacker| { + if let (Some(attacker), Some(target)) = + (players.get(attacker), players.get(entity_b)) + { + attacker.disallow_harm(target) + } else { + false + } + }) + }; + + 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, target_group, + avoid_harm, + }; + + attack.apply_attack( attacker_info, target_info, dir, - false, + attack_options, strength, combat::AttackSource::Explosion, |e| server_eventbus.emit_now(e), @@ -902,6 +922,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o } }, RadiusEffect::Entity(mut effect) => { + let players = &ecs.read_storage::(); for (entity_b, pos_b, body_b_maybe) in ( &ecs.entities(), &ecs.read_storage::(), @@ -921,9 +942,23 @@ pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, o .read_storage::() .get(entity_b) .map_or(true, |h| !h.is_dead); + if is_alive { + let avoid_harm = { + owner_entity.map_or(false, |attacker| { + if let (Some(attacker), Some(target)) = + (players.get(attacker), players.get(entity_b)) + { + attacker.disallow_harm(target) + } else { + false + } + }) + }; effect.modify_strength(strength); - server.state().apply_effect(entity_b, effect.clone(), owner); + if !(effect.is_harm() && avoid_harm) { + server.state().apply_effect(entity_b, effect.clone(), owner); + } } } } diff --git a/server/src/settings.rs b/server/src/settings.rs index ce66969277..db41af5a5d 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -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, 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), diff --git a/server/src/sys/msg/register.rs b/server/src/sys/msg/register.rs index 8d3015d946..00923eb490 100644 --- a/server/src/sys/msg/register.rs +++ b/server/src/sys/msg/register.rs @@ -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>; +#[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>, + _health_comp: 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>, + ReadData<'a>, ReadExpect<'a, PlayerMetrics>, - ReadStorage<'a, Health>, - ReadStorage<'a, Uid>, - ReadStorage<'a, Client>, - WriteStorage<'a, Player>, - WriteStorage<'a, PendingLogin>, - Read<'a, UidAllocator>, - ReadPlugin<'a>, - ReadStorage<'a, Stats>, - WriteExpect<'a, LoginProvider>, - WriteStorage<'a, Admin>, + ReadExpect<'a, Settings>, ReadExpect<'a, EditableSettings>, - Read<'a, EventBus>, + WriteStorage<'a, Player>, + WriteStorage<'a, Admin>, + WriteStorage<'a, PendingLogin>, + WriteExpect<'a, LoginProvider>, ); const NAME: &'static str = "msg::register"; @@ -58,24 +65,23 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, ( - entities, + read_data, player_metrics, - _health_comp, // used by plugin feature - uids, - clients, - mut players, - mut pending_logins, - _uid_allocator, // used by plugin feature - _plugin_mgr, // used by plugin feature - stats, - mut login_provider, - mut admins, + settings, editable_settings, - server_event_bus, + mut players, + mut admins, + mut pending_logins, + mut login_provider, ): Self::SystemData, ) { // 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 +98,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 +109,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._health_comp).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,7 +127,7 @@ impl<'a> System<'a> for Sys { #[cfg(feature = "plugins")] &ecs_world, #[cfg(feature = "plugins")] - &_plugin_mgr, + &read_data._plugin_mgr, &*editable_settings.admins, &*editable_settings.whitelist, &*editable_settings.banlist, @@ -130,10 +138,12 @@ impl<'a> System<'a> for Sys { trace!(?r, "pending login returned"); match r { Err(e) => { - server_event_bus.emit_now(ServerEvent::ClientDisconnect( - entity, - common::comp::DisconnectReason::Kicked, - )); + read_data + .server_event_bus + .emit_now(ServerEvent::ClientDisconnect( + entity, + common::comp::DisconnectReason::Kicked, + )); client.send(ServerRegisterAnswer::Err(e))?; return Ok(()); }, @@ -143,15 +153,18 @@ impl<'a> System<'a> for Sys { }; // Check if user is already logged-in - if let Some((old_entity, old_client, _)) = (&entities, &clients, &players) - .join() - .find(|(_, _, old_player)| old_player.uuid() == uuid) + 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( - old_entity, - common::comp::DisconnectReason::NewerLogin, - )); + read_data + .server_event_bus + .emit_now(ServerEvent::ClientDisconnect( + old_entity, + common::comp::DisconnectReason::NewerLogin, + )); let _ = old_client.send(ServerGeneral::Disconnect(DisconnectReason::Kicked( String::from("You have logged in from another location."), ))); @@ -166,7 +179,7 @@ impl<'a> System<'a> for Sys { return Ok(()); } - let player = Player::new(username, uuid); + let player = Player::new(username, settings.battle_mode, uuid); let admin = editable_settings.admins.get(&uuid); if !player.is_valid() { @@ -215,9 +228,9 @@ 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)) { + if let (Some(uid), Some(player)) = (read_data.uids.get(entity), players.get(entity)) { 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 {