diff --git a/assets/common/abilities/unique/mindflayer/dimensionaldoor.ron b/assets/common/abilities/unique/mindflayer/dimensionaldoor.ron index ca0309ede7..6fefdb4a2d 100644 --- a/assets/common/abilities/unique/mindflayer/dimensionaldoor.ron +++ b/assets/common/abilities/unique/mindflayer/dimensionaldoor.ron @@ -1 +1,5 @@ -BasicBlock \ No newline at end of file +Blink( + buildup_duration: 0.5, + recover_duration: 0.25, + max_range: 100.0, +) \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index 13e20f5ccf..a7505d79fe 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -999,11 +999,17 @@ impl Client { } } - pub fn handle_input(&mut self, input: InputKind, pressed: bool, select_pos: Option>) { + pub fn handle_input( + &mut self, + input: InputKind, + pressed: bool, + select_pos: Option>, + target_entity: Option, + ) { if pressed { self.control_action(ControlAction::StartInput { input, - target: None, + target_entity: target_entity.and_then(|e| self.state.read_component_copied(e)), select_pos, }); } else { @@ -1638,6 +1644,15 @@ impl Client { impulse, }); }, + ServerGeneral::PositionUpdate(pos) => { + self.state + .ecs() + .read_resource::>() + .emit_now(LocalEvent::PositionUpdate { + entity: self.entity(), + pos, + }); + }, ServerGeneral::UpdatePendingTrade(id, trade, pricing) => { tracing::trace!("UpdatePendingTrade {:?} {:?}", id, trade); self.pending_trade = Some((id, trade, pricing)); diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index b16a10dbfa..958fbb8be7 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -104,6 +104,7 @@ pub enum ServerGeneral { SetViewDistance(u32), Outcomes(Vec), Knockback(Vec3), + PositionUpdate(comp::Pos), // Ingame related AND terrain stream TerrainChunkUpdate { key: Vec2, @@ -235,6 +236,7 @@ impl ServerMsg { | ServerGeneral::SetViewDistance(_) | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) + | ServerGeneral::PositionUpdate(_) | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) | ServerGeneral::SiteEconomy(_) => { diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 07a80cc7ae..c4808bb94b 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1,6 +1,6 @@ use crate::{ assets::{self, Asset}, - combat::{self, CombatEffect}, + combat::{self, CombatEffect, Knockback}, comp::{ aura, beam, inventory::item::tool::ToolKind, projectile::ProjectileConstructor, skills, Body, CharacterState, EnergySource, Gravity, LightEmitter, StateUpdate, @@ -10,7 +10,6 @@ use crate::{ utils::{AbilityInfo, StageSection}, *, }, - Knockback, }; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -252,6 +251,11 @@ pub enum CharacterAbility { energy_cost: f32, specifier: beam::FrontendSpecifier, }, + Blink { + buildup_duration: f32, + recover_duration: f32, + max_range: f32, + }, } impl Default for CharacterAbility { @@ -532,6 +536,14 @@ impl CharacterAbility { *heal *= power; *tick_rate *= speed; }, + Blink { + ref mut buildup_duration, + ref mut recover_duration, + .. + } => { + *buildup_duration /= speed; + *recover_duration /= speed; + }, } self } @@ -558,7 +570,7 @@ impl CharacterAbility { 0 } }, - BasicBlock | Boost { .. } | ComboMelee { .. } => 0, + BasicBlock | Boost { .. } | ComboMelee { .. } | Blink { .. } => 0, } } @@ -1560,6 +1572,20 @@ impl From<(&CharacterAbility, AbilityInfo)> for CharacterState { timer: Duration::default(), stage_section: StageSection::Buildup, }), + CharacterAbility::Blink { + buildup_duration, + recover_duration, + max_range, + } => CharacterState::Blink(blink::Data { + static_data: blink::StaticData { + buildup_duration: Duration::from_secs_f32(*buildup_duration), + recover_duration: Duration::from_secs_f32(*recover_duration), + max_range: *max_range, + ability_info, + }, + timer: Duration::default(), + stage_section: StageSection::Buildup, + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index baca44b4ae..759a81ce6e 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -93,6 +93,8 @@ pub enum CharacterState { /// specifically for the healing beam. There was also functionality present /// on basic beam which was unnecessary for the healing beam. HealingBeam(healing_beam::Data), + /// A short teleport that targets either a position or entity + Blink(blink::Data), } impl CharacterState { diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index f0a32840aa..0db1f7be95 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -110,7 +110,7 @@ pub enum ControlAction { Talk, StartInput { input: InputKind, - target: Option, + target_entity: Option, // Some inputs need a selected position, such as mining select_pos: Option>, }, @@ -121,7 +121,7 @@ impl ControlAction { pub fn basic_input(input: InputKind) -> Self { ControlAction::StartInput { input, - target: None, + target_entity: None, select_pos: None, } } @@ -144,9 +144,10 @@ impl InputKind { } } -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct InputAttr { pub select_pos: Option>, + pub target_entity: Option, } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] diff --git a/common/src/event.rs b/common/src/event.rs index c146274846..51b7c8d451 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -28,6 +28,8 @@ pub enum LocalEvent { }, /// Applies `vel` velocity to `entity` Boost { entity: EcsEntity, vel: Vec3 }, + /// Updates the position of the entity + PositionUpdate { entity: EcsEntity, pos: Pos }, } #[allow(clippy::large_enum_variant)] // TODO: Pending review in #587 @@ -161,6 +163,11 @@ pub enum ServerEvent { pos: Vec3, tool: Option, }, + TeleportTo { + entity: EcsEntity, + target: Uid, + max_range: Option, + }, } pub struct EventBus { diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index 961a52d366..a650270331 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -34,11 +34,14 @@ pub trait CharacterBehavior { &self, data: &JoinData, input: InputKind, - _target: Option, + target_entity: Option, select_pos: Option>, ) -> StateUpdate { let mut update = StateUpdate::from(data); - update.queued_inputs.insert(input, InputAttr { select_pos }); + update.queued_inputs.insert(input, InputAttr { + select_pos, + target_entity, + }); update } fn cancel_input(&self, data: &JoinData, input: InputKind) -> StateUpdate { @@ -60,9 +63,9 @@ pub trait CharacterBehavior { ControlAction::Talk => self.talk(data), ControlAction::StartInput { input, - target, + target_entity, select_pos, - } => self.start_input(data, input, target, select_pos), + } => self.start_input(data, input, target_entity, select_pos), ControlAction::CancelInput(input) => self.cancel_input(data, input), } } diff --git a/common/src/states/blink.rs b/common/src/states/blink.rs new file mode 100644 index 0000000000..1814eef99c --- /dev/null +++ b/common/src/states/blink.rs @@ -0,0 +1,94 @@ +use crate::{ + comp::{CharacterState, StateUpdate}, + event::ServerEvent, + states::{ + behavior::{CharacterBehavior, JoinData}, + utils::*, + }, +}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +/// Separated out to condense update portions of character state +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StaticData { + pub buildup_duration: Duration, + pub recover_duration: Duration, + pub max_range: f32, + pub ability_info: AbilityInfo, +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + /// Struct containing data that does not change over the course of the + /// character state + pub static_data: StaticData, + /// Timer for each stage + pub timer: Duration, + /// What section the character stage is in + pub stage_section: StageSection, +} + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + + match self.stage_section { + StageSection::Buildup => { + if self.timer < self.static_data.buildup_duration { + // Build up + update.character = CharacterState::Blink(Data { + timer: self + .timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + ..*self + }); + } else { + // Blinks to target location, defaults to 25 meters in front if no target + // provided + if let Some(input_attr) = self.static_data.ability_info.input_attr { + if let Some(target) = input_attr.target_entity { + update.server_events.push_front(ServerEvent::TeleportTo { + entity: data.entity, + target, + max_range: Some(self.static_data.max_range), + }); + } else if let Some(pos) = input_attr.select_pos { + update.pos.0 = pos; + } else { + update.pos.0 += *data.inputs.look_dir * 25.0; + } + } + // Transitions to recover section of stage + update.character = CharacterState::Blink(Data { + timer: Duration::default(), + stage_section: StageSection::Recover, + ..*self + }); + } + }, + StageSection::Recover => { + if self.timer < self.static_data.recover_duration { + // Recovery + update.character = CharacterState::Blink(Data { + timer: self + .timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + ..*self + }); + } else { + // Done + update.character = CharacterState::Wielding; + } + }, + _ => { + // If it somehow ends up in an incorrect stage section + update.character = CharacterState::Wielding; + }, + } + + update + } +} diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 049f6a058c..519a6f432a 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -4,6 +4,7 @@ pub mod basic_block; pub mod basic_melee; pub mod basic_ranged; pub mod behavior; +pub mod blink; pub mod boost; pub mod charged_melee; pub mod charged_ranged; diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 53d4f1693d..b0c7328013 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -5,7 +5,8 @@ use crate::{ item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, quadruped_small, ship, skills::{Skill, SwimSkill}, - theropod, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, + theropod, Body, CharacterAbility, CharacterState, InputAttr, InputKind, InventoryAction, + StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, event::{LocalEvent, ServerEvent}, @@ -523,11 +524,10 @@ fn handle_ability(data: &JoinData, update: &mut StateUpdate, input: InputKind) { }) .filter(|ability| ability.requirements_paid(data, update)) { - update.character = ( + update.character = CharacterState::from(( &ability, AbilityInfo::from_input(data, matches!(equip_slot, EquipSlot::Offhand), input), - ) - .into(); + )); } } } @@ -578,40 +578,23 @@ pub fn handle_dodge_input(data: &JoinData, update: &mut StateUpdate) { }) .filter(|ability| ability.requirements_paid(data, update)) { + update.character = CharacterState::from(( + &ability, + AbilityInfo::from_input(data, false, InputKind::Roll), + )); if let CharacterState::ComboMelee(c) = data.character { - update.character = ( - &ability, - AbilityInfo::from_input(data, false, InputKind::Roll), - ) - .into(); if let CharacterState::Roll(roll) = &mut update.character { roll.was_combo = Some((c.static_data.ability_info.input, c.stage)); roll.was_wielded = true; } } else if data.character.is_wield() { - update.character = ( - &ability, - AbilityInfo::from_input(data, false, InputKind::Roll), - ) - .into(); if let CharacterState::Roll(roll) = &mut update.character { roll.was_wielded = true; } } else if data.character.is_stealthy() { - update.character = ( - &ability, - AbilityInfo::from_input(data, false, InputKind::Roll), - ) - .into(); if let CharacterState::Roll(roll) = &mut update.character { roll.was_sneak = true; } - } else { - update.character = ( - &ability, - AbilityInfo::from_input(data, false, InputKind::Roll), - ) - .into(); } } } @@ -707,7 +690,7 @@ pub struct AbilityInfo { pub tool: Option, pub hand: Option, pub input: InputKind, - pub select_pos: Option>, + pub input_attr: Option, } impl AbilityInfo { @@ -730,13 +713,7 @@ impl AbilityInfo { tool, hand, input, - select_pos: data - .controller - .queued_inputs - .get(&input) - .cloned() - .unwrap_or_default() - .select_pos, + input_attr: data.controller.queued_inputs.get(&input).copied(), } } } diff --git a/common/sys/src/character_behavior.rs b/common/sys/src/character_behavior.rs index f266431126..2a02313a64 100644 --- a/common/sys/src/character_behavior.rs +++ b/common/sys/src/character_behavior.rs @@ -302,6 +302,7 @@ impl<'a> System<'a> for Sys { CharacterState::BasicBeam(data) => data.handle_event(&j, action), CharacterState::BasicAura(data) => data.handle_event(&j, action), CharacterState::HealingBeam(data) => data.handle_event(&j, action), + CharacterState::Blink(data) => data.handle_event(&j, action), }; local_emitter.append(&mut state_update.local_events); server_emitter.append(&mut state_update.server_events); @@ -354,6 +355,7 @@ impl<'a> System<'a> for Sys { CharacterState::BasicBeam(data) => data.behavior(&j), CharacterState::BasicAura(data) => data.behavior(&j), CharacterState::HealingBeam(data) => data.behavior(&j), + CharacterState::Blink(data) => data.behavior(&j), }; local_emitter.append(&mut state_update.local_events); diff --git a/common/sys/src/state.rs b/common/sys/src/state.rs index 5f09492481..e7aee77b24 100644 --- a/common/sys/src/state.rs +++ b/common/sys/src/state.rs @@ -489,6 +489,7 @@ impl State { let events = self.ecs.read_resource::>().recv_all(); for event in events { let mut velocities = self.ecs.write_storage::(); + let mut positions = self.ecs.write_storage::(); let physics = self.ecs.read_storage::(); match event { LocalEvent::Jump(entity, impulse) => { @@ -509,6 +510,11 @@ impl State { vel.0 += extra_vel; } }, + LocalEvent::PositionUpdate { entity, pos } => { + if let Some(position) = positions.get_mut(entity) { + *position = pos; + } + }, } } drop(guard); diff --git a/common/sys/src/stats.rs b/common/sys/src/stats.rs index 2304691d95..6a92a08887 100644 --- a/common/sys/src/stats.rs +++ b/common/sys/src/stats.rs @@ -246,7 +246,8 @@ impl<'a> System<'a> for Sys { | CharacterState::Shockwave { .. } | CharacterState::BasicBeam { .. } | CharacterState::BasicAura { .. } - | CharacterState::HealingBeam { .. } => { + | CharacterState::HealingBeam { .. } + | CharacterState::Blink { .. } => { if energy.get_unchecked().regen_rate != 0.0 { energy.get_mut_unchecked().regen_rate = 0.0 } diff --git a/server/src/client.rs b/server/src/client.rs index 9e03187278..7f3495cd1d 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -110,6 +110,7 @@ impl Client { | ServerGeneral::SiteEconomy(_) | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) + | ServerGeneral::PositionUpdate(_) | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) => { self.in_game_stream.lock().unwrap().send(g) @@ -180,6 +181,7 @@ impl Client { | ServerGeneral::SetViewDistance(_) | ServerGeneral::Outcomes(_) | ServerGeneral::Knockback(_) + | ServerGeneral::PositionUpdate(_) | ServerGeneral::SiteEconomy(_) | ServerGeneral::UpdatePendingTrade(_, _, _) | ServerGeneral::FinishedTrade(_) => { diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index c8b8934ca9..eb5ad2225e 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -941,3 +941,25 @@ pub fn handle_combo_change(server: &Server, entity: EcsEntity, change: i32) { } } } + +pub fn handle_teleport_to(server: &Server, entity: EcsEntity, target: Uid, max_range: Option) { + let ecs = &server.state.ecs(); + let mut positions = ecs.write_storage::(); + let clients = ecs.read_storage::(); + + let target_pos = server + .state + .ecs() + .entity_from_uid(target.into()) + .and_then(|e| positions.get(e)) + .copied(); + + if let (Some(pos), Some(target_pos)) = (positions.get_mut(entity), target_pos) { + if max_range.map_or(true, |r| pos.0.distance_squared(target_pos.0) < r.powi(2)) { + *pos = target_pos; + if let Some(client) = clients.get(entity) { + client.send_fallible(ServerGeneral::PositionUpdate(target_pos)); + } + } + } +} diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index c5cf32c982..348b0043ca 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -8,7 +8,7 @@ use entity_creation::{ use entity_manipulation::{ handle_aura, handle_buff, handle_combo_change, handle_damage, handle_delete, handle_destroy, handle_energy_change, handle_explosion, handle_knockback, handle_land_on_ground, handle_poise, - handle_respawn, + handle_respawn, handle_teleport_to, }; use group_manip::handle_group; use information::handle_site_info; @@ -199,6 +199,11 @@ impl Server { }, ServerEvent::RequestSiteInfo { entity, id } => handle_site_info(&self, entity, id), ServerEvent::MineBlock { pos, tool } => handle_mine_block(self, pos, tool), + ServerEvent::TeleportTo { + entity, + target, + max_range, + } => handle_teleport_to(&self, entity, target, max_range), } } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index a2c3e2952f..9e2d78ed03 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -363,6 +363,7 @@ impl<'a> System<'a> for Sys { tgt_pos, read_data.bodies.get(attacker), &read_data.dt, + &read_data, ); } } @@ -443,6 +444,7 @@ impl<'a> System<'a> for Sys { tgt_pos, read_data.bodies.get(attacker), &read_data.dt, + &read_data, ); // Remember this encounter if an RtSim entity if let Some(tgt_stats) = @@ -604,6 +606,7 @@ impl<'a> AgentData<'a> { tgt_pos, read_data.bodies.get(target), &read_data.dt, + &read_data, ); } else { agent.target = None; @@ -1177,6 +1180,7 @@ impl<'a> AgentData<'a> { } } + #[allow(clippy::too_many_arguments)] fn attack( &self, agent: &mut Agent, @@ -1185,6 +1189,7 @@ impl<'a> AgentData<'a> { tgt_pos: &Pos, tgt_body: Option<&Body>, dt: &DeltaTime, + _read_data: &ReadData, ) { let min_attack_dist = self.body.map_or(3.0, |b| b.radius() * self.scale + 2.0); let tactic = match self diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a7c04828b3..8a5a66be2d 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -390,7 +390,12 @@ impl PlayState for SessionState { client.remove_block(select_pos.map(|e| e.floor() as i32)); } } else { - client.handle_input(InputKind::Primary, state, select_pos); + client.handle_input( + InputKind::Primary, + state, + select_pos, + target_entity.map(|t| t.0), + ); } }, GameInput::Secondary => { @@ -404,7 +409,12 @@ impl PlayState for SessionState { ); } } else { - client.handle_input(InputKind::Secondary, state, select_pos); + client.handle_input( + InputKind::Secondary, + state, + select_pos, + target_entity.map(|t| t.0), + ); } }, GameInput::Roll => { @@ -423,7 +433,12 @@ impl PlayState for SessionState { } } } else { - client.handle_input(InputKind::Roll, state, select_pos); + client.handle_input( + InputKind::Roll, + state, + select_pos, + target_entity.map(|t| t.0), + ); } }, GameInput::Respawn => { @@ -434,7 +449,12 @@ impl PlayState for SessionState { }, GameInput::Jump => { let mut client = self.client.borrow_mut(); - client.handle_input(InputKind::Jump, state, select_pos); + client.handle_input( + InputKind::Jump, + state, + select_pos, + target_entity.map(|t| t.0), + ); }, GameInput::SwimUp => { self.key_state.swim_up = state; @@ -495,7 +515,12 @@ impl PlayState for SessionState { // controller change self.key_state.fly ^= state; let mut client = self.client.borrow_mut(); - client.handle_input(InputKind::Fly, self.key_state.fly, select_pos); + client.handle_input( + InputKind::Fly, + self.key_state.fly, + select_pos, + target_entity.map(|t| t.0), + ); }, GameInput::Climb => { self.key_state.climb_up = state; @@ -1254,11 +1279,21 @@ impl PlayState for SessionState { }, HudEvent::Ability3(state) => { let mut client = self.client.borrow_mut(); - client.handle_input(InputKind::Ability(0), state, select_pos); + client.handle_input( + InputKind::Ability(0), + state, + select_pos, + target_entity.map(|t| t.0), + ); }, HudEvent::Ability4(state) => { let mut client = self.client.borrow_mut(); - client.handle_input(InputKind::Ability(1), state, select_pos); + client.handle_input( + InputKind::Ability(1), + state, + select_pos, + target_entity.map(|t| t.0), + ); }, HudEvent::ChangeFOV(new_fov) => { global_state.settings.graphics.fov = new_fov;