diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 7c1a2d6ebf..614d86bb72 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -47,6 +47,8 @@ pub enum ChatCommand { Alias, ApplyBuff, Ban, + BattleMode, + BattleModeForce, Build, BuildAreaAdd, BuildAreaList, @@ -321,6 +323,28 @@ impl ChatCommand { true for overwrite to alter an existing ban..", Some(Moderator), ), + #[rustfmt::skip] + ChatCommand::BattleMode => cmd( + vec![Enum( + "battle mode", + vec!["pvp".to_owned(), "pve".to_owned()], + Optional, + )], + "Set your battle mode to:\n\ + * pvp (player vs player)\n\ + * pve (player vs environment).\n\ + If called without arguments will show current battle mode.", + None, + ), + ChatCommand::BattleModeForce => cmd( + vec![Enum( + "battle mode", + vec!["pvp".to_owned(), "pve".to_owned()], + Required, + )], + "Change your battle mode flag without any checks", + Some(Admin), + ), ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", None), ChatCommand::BuildAreaAdd => cmd( vec![ @@ -623,6 +647,8 @@ impl ChatCommand { ChatCommand::Alias => "alias", ChatCommand::ApplyBuff => "buff", ChatCommand::Ban => "ban", + ChatCommand::BattleMode => "battlemode", + ChatCommand::BattleModeForce => "battlemode_force", ChatCommand::Build => "build", ChatCommand::BuildAreaAdd => "build_area_add", ChatCommand::BuildAreaList => "build_area_list", diff --git a/common/src/combat.rs b/common/src/combat.rs index e8fb6270da..f2d02722b7 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -12,13 +12,13 @@ use crate::{ }, poise::PoiseChange, skills::SkillGroupKind, - Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health, HealthChange, - HealthSource, Inventory, Ori, Player, Poise, SkillSet, Stats, + Alignment, Body, CharacterState, Combo, Energy, EnergyChange, EnergySource, Health, + HealthChange, HealthSource, Inventory, Ori, Player, Poise, SkillSet, Stats, }, event::ServerEvent, outcome::Outcome, states::utils::StageSection, - uid::Uid, + uid::{Uid, UidAllocator}, util::Dir, }; @@ -28,7 +28,7 @@ use rand::{thread_rng, Rng}; use serde::{Deserialize, Serialize}; #[cfg(not(target_arch = "wasm32"))] -use specs::Entity as EcsEntity; +use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage}; #[cfg(not(target_arch = "wasm32"))] use std::{ops::MulAssign, time::Duration}; #[cfg(not(target_arch = "wasm32"))] use vek::*; @@ -469,15 +469,56 @@ 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)) +/// Function that checks for unintentional PvP between players. +/// +/// Returns `false` if attack will create unintentional conflict, +/// e.g. if player with PvE mode will harm pets of other players +/// or other players will do the same to such player. +/// +/// If both players have PvP mode enabled, interact with NPC and +/// in any other case, this function will return `true` +// TODO: add parameter for doing self-harm? +pub fn may_harm( + alignments: &ReadStorage, + players: &ReadStorage, + uid_allocator: &UidAllocator, + attacker: Option, + target: EcsEntity, +) -> bool { + // Return owner entity if pet, + // or just return entity back otherwise + let owner_if_pet = |entity| { + let alignment = alignments.get(entity).copied(); + if let Some(Alignment::Owned(uid)) = alignment { + // return original entity + // if can't get owner + uid_allocator + .retrieve_entity_internal(uid.into()) + .unwrap_or(entity) + } else { + entity + } + }; + + // Just return ok if attacker is unknown, it's probably + // environment or command. + let attacker = match attacker { + Some(attacker) => attacker, + None => return true, + }; + + // "Dereference" to owner if this is a pet. + let attacker = owner_if_pet(attacker); + let target = owner_if_pet(target); + + // Get player components + let attacker_info = players.get(attacker); + let target_info = players.get(target); + + // Return `true` if not players. + attacker_info + .zip(target_info) + .map_or(true, |(a, t)| a.may_harm(t)) } #[cfg(not(target_arch = "wasm32"))] diff --git a/common/src/comp/player.rs b/common/src/comp/player.rs index 76db8ddc55..8eecf6b90d 100644 --- a/common/src/comp/player.rs +++ b/common/src/comp/player.rs @@ -3,7 +3,7 @@ use specs::{Component, DerefFlaggedStorage, NullStorage}; use specs_idvs::IdvStorage; use uuid::Uuid; -use crate::resources::BattleMode; +use crate::resources::{BattleMode, Time}; const MAX_ALIAS_LEN: usize = 32; @@ -20,6 +20,7 @@ pub enum DisconnectReason { pub struct Player { pub alias: String, pub battle_mode: BattleMode, + pub last_battlemode_change: Option