Add battle_mode server setting and player flag

This commit is contained in:
juliancoffee 2021-07-26 16:52:43 +03:00
parent e6ef678c28
commit 7d4a8cbfa4
11 changed files with 288 additions and 76 deletions

View File

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

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

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.
///
/// 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,
}

View File

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

View File

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

View File

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

View File

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

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,31 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, 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<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>(),
@ -921,9 +942,23 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
.read_storage::<comp::Health>()
.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);
}
}
}
}

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>>,
_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<ServerEvent>>,
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<Self>,
(
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 {