veloren/common/src/states/utils.rs

1149 lines
42 KiB
Rust
Raw Normal View History

2020-02-03 10:54:50 +00:00
use crate::{
astar::Astar,
combat,
comp::{
2021-02-20 21:56:10 +00:00
biped_large, biped_small,
character_state::OutputEvents,
inventory::slot::{EquipSlot, Slot},
item::{Hands, ItemKind, Tool, ToolKind},
quadruped_low, quadruped_medium, quadruped_small,
2021-08-22 10:50:01 +00:00
skills::{Skill, SwimSkill, SKILL_MODIFIERS},
theropod, Body, CharacterAbility, CharacterState, Density, InputAttr, InputKind,
InventoryAction, StateUpdate,
},
consts::{FRIC_GROUND, GRAVITY, MAX_PICKUP_RANGE},
event::{LocalEvent, ServerEvent},
states::{behavior::JoinData, *},
util::Dir,
vol::ReadVol,
2019-12-28 16:10:39 +00:00
};
use core::hash::BuildHasherDefault;
use fxhash::FxHasher64;
use serde::{Deserialize, Serialize};
2021-06-23 02:08:34 +00:00
use std::{
ops::{Add, Div},
time::Duration,
};
use strum_macros::Display;
use vek::*;
2020-03-08 19:37:17 +00:00
pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
2020-03-08 15:38:53 +00:00
2020-07-03 18:30:41 +00:00
impl Body {
pub fn base_accel(&self) -> f32 {
match self {
Body::Humanoid(_) => 100.0,
Body::QuadrupedSmall(body) => match body.species {
quadruped_small::Species::Turtle => 30.0,
quadruped_small::Species::Axolotl => 70.0,
quadruped_small::Species::Pig => 70.0,
quadruped_small::Species::Sheep => 70.0,
quadruped_small::Species::Cat => 70.0,
quadruped_small::Species::Truffler => 70.0,
quadruped_small::Species::Fungome => 70.0,
2021-03-12 00:20:05 +00:00
quadruped_small::Species::Goat => 80.0,
_ => 125.0,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Grolgar => 110.0,
quadruped_medium::Species::Saber => 180.0,
quadruped_medium::Species::Tiger => 150.0,
quadruped_medium::Species::Tuskram => 160.0,
quadruped_medium::Species::Lion => 170.0,
quadruped_medium::Species::Tarasque => 100.0,
quadruped_medium::Species::Wolf => 180.0,
quadruped_medium::Species::Frostfang => 180.0,
quadruped_medium::Species::Mouflon => 100.0,
quadruped_medium::Species::Catoblepas => 70.0,
quadruped_medium::Species::Bonerattler => 130.0,
quadruped_medium::Species::Deer => 150.0,
quadruped_medium::Species::Hirdrasil => 160.0,
2020-11-20 06:11:13 +00:00
quadruped_medium::Species::Roshwalr => 160.0,
quadruped_medium::Species::Donkey => 110.0,
2020-11-20 06:11:13 +00:00
quadruped_medium::Species::Camel => 75.0,
quadruped_medium::Species::Zebra => 150.0,
quadruped_medium::Species::Antelope => 185.0,
quadruped_medium::Species::Kelpie => 180.0,
quadruped_medium::Species::Horse => 180.0,
2021-02-13 01:01:07 +00:00
quadruped_medium::Species::Barghest => 80.0,
quadruped_medium::Species::Cattle => 80.0,
quadruped_medium::Species::Darkhound => 160.0,
quadruped_medium::Species::Highland => 80.0,
quadruped_medium::Species::Yak => 90.0,
quadruped_medium::Species::Panda => 90.0,
quadruped_medium::Species::Bear => 90.0,
2021-03-12 00:20:05 +00:00
quadruped_medium::Species::Dreadhorn => 140.0,
quadruped_medium::Species::Moose => 130.0,
quadruped_medium::Species::Snowleopard => 160.0,
2021-06-13 23:39:43 +00:00
quadruped_medium::Species::Mammoth => 180.0,
quadruped_medium::Species::Ngoubou => 170.0,
2021-06-22 23:43:18 +00:00
quadruped_medium::Species::Llama => 120.0,
quadruped_medium::Species::Alpaca => 110.0,
},
2021-02-20 20:38:27 +00:00
Body::BipedLarge(body) => match body.species {
biped_large::Species::Slysaurok => 100.0,
biped_large::Species::Occultsaurok => 100.0,
biped_large::Species::Mightysaurok => 100.0,
biped_large::Species::Mindflayer => 90.0,
2021-05-01 13:36:45 +00:00
biped_large::Species::Minotaur => 60.0,
2021-06-23 20:15:05 +00:00
biped_large::Species::Huskbrute => 130.0,
biped_large::Species::Cultistwarlord => 110.0,
biped_large::Species::Cultistwarlock => 90.0,
2021-02-20 20:38:27 +00:00
_ => 80.0,
2021-02-20 21:56:10 +00:00
},
Body::BirdMedium(_) => 80.0,
2020-11-26 21:57:38 +00:00
Body::FishMedium(_) => 80.0,
2020-07-03 18:30:41 +00:00
Body::Dragon(_) => 250.0,
2021-04-17 10:38:00 +00:00
Body::BirdLarge(_) => 110.0,
2020-11-26 21:57:38 +00:00
Body::FishSmall(_) => 60.0,
2021-02-20 20:38:27 +00:00
Body::BipedSmall(biped_small) => match biped_small.species {
biped_small::Species::Haniwa => 65.0,
_ => 80.0,
2021-02-26 06:19:46 +00:00
},
2021-01-18 21:15:18 +00:00
Body::Object(_) => 0.0,
2020-07-30 06:13:58 +00:00
Body::Golem(_) => 60.0,
2020-08-30 07:40:40 +00:00
Body::Theropod(_) => 135.0,
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
quadruped_low::Species::Crocodile => 130.0,
quadruped_low::Species::Alligator => 110.0,
quadruped_low::Species::Salamander => 85.0,
quadruped_low::Species::Monitor => 160.0,
2021-06-15 05:43:49 +00:00
quadruped_low::Species::Asp => 110.0,
quadruped_low::Species::Tortoise => 60.0,
quadruped_low::Species::Rocksnapper => 70.0,
quadruped_low::Species::Pangolin => 120.0,
quadruped_low::Species::Maneater => 80.0,
2020-11-20 06:11:13 +00:00
quadruped_low::Species::Sandshark => 160.0,
quadruped_low::Species::Hakulaq => 140.0,
2020-11-21 19:06:36 +00:00
quadruped_low::Species::Lavadrake => 100.0,
2021-06-15 05:43:49 +00:00
quadruped_low::Species::Basilisk => 90.0,
2021-02-18 00:41:34 +00:00
quadruped_low::Species::Deadwood => 140.0,
},
Body::Ship(_) => 0.0,
2020-07-03 18:30:41 +00:00
}
}
2020-07-07 00:01:39 +00:00
pub fn air_accel(&self) -> f32 { self.base_accel() * 0.025 }
/// Attempt to determine the maximum speed of the character
/// when moving on the ground
pub fn max_speed_approx(&self) -> f32 {
// Inverse kinematics: at what velocity will acceleration
// be cancelled out by friction drag?
// Note: we assume no air, since it's such a small factor.
// Derived via...
// v = (v + dv / 30) * (1 - drag).powi(2) (accel cancels drag)
// => 1 = (1 + (dv / 30) / v) * (1 - drag).powi(2)
// => 1 / (1 - drag).powi(2) = 1 + (dv / 30) / v
// => 1 / (1 - drag).powi(2) - 1 = (dv / 30) / v
// => 1 / (1 / (1 - drag).powi(2) - 1) = v / (dv / 30)
// => (dv / 30) / (1 / (1 - drag).powi(2) - 1) = v
let v = (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powi(2) - 1.0);
debug_assert!(v >= 0.0, "Speed must be positive!");
v
}
/// The turn rate in 180°/s (or (rotations per second)/2)
2020-07-07 00:01:39 +00:00
pub fn base_ori_rate(&self) -> f32 {
match self {
2021-05-01 16:29:01 +00:00
Body::Humanoid(_) => 3.5,
Body::QuadrupedSmall(_) => 3.0,
2021-06-23 20:15:05 +00:00
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Mammoth => 2.2,
_ => 2.8,
},
Body::BirdMedium(_) => 6.0,
Body::FishMedium(_) => 6.0,
Body::Dragon(_) => 1.0,
Body::BirdLarge(_) => 7.0,
Body::FishSmall(_) => 7.0,
2021-05-01 16:29:01 +00:00
Body::BipedLarge(_) => 2.7,
Body::BipedSmall(_) => 3.5,
Body::Object(_) => 2.0,
2021-05-01 16:29:01 +00:00
Body::Golem(_) => 2.0,
2020-11-21 20:59:25 +00:00
Body::Theropod(theropod) => match theropod.species {
2021-05-01 16:29:01 +00:00
theropod::Species::Archaeos => 2.3,
theropod::Species::Odonto => 2.3,
theropod::Species::Ntouka => 2.3,
_ => 2.5,
2020-11-21 20:59:25 +00:00
},
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
2021-05-01 16:29:01 +00:00
quadruped_low::Species::Asp => 2.2,
quadruped_low::Species::Tortoise => 1.5,
quadruped_low::Species::Rocksnapper => 1.8,
quadruped_low::Species::Lavadrake => 1.7,
_ => 2.0,
2020-11-21 20:59:25 +00:00
},
Body::Ship(ship) if ship.has_water_thrust() => 0.1,
Body::Ship(_) => 0.035,
2020-07-07 00:01:39 +00:00
}
}
2020-11-03 22:46:07 +00:00
/// Returns thrust force if the body type can swim, otherwise None
pub fn swim_thrust(&self) -> Option<f32> {
2021-03-12 14:56:47 +00:00
match self {
Body::Object(_) => None,
Body::BipedLarge(_) | Body::Golem(_) => Some(200.0 * self.mass().0),
Body::BipedSmall(_) => Some(100.0 * self.mass().0),
Body::BirdMedium(_) => Some(50.0 * self.mass().0),
Body::BirdLarge(_) => Some(50.0 * self.mass().0),
Body::FishMedium(_) => Some(50.0 * self.mass().0),
Body::FishSmall(_) => Some(50.0 * self.mass().0),
Body::Dragon(_) => Some(200.0 * self.mass().0),
Body::Humanoid(_) => Some(2500.0 * self.mass().0),
Body::Theropod(body) => match body.species {
theropod::Species::Sandraptor
| theropod::Species::Snowraptor
| theropod::Species::Sunlizard
| theropod::Species::Woodraptor
| theropod::Species::Yale => Some(200.0 * self.mass().0),
_ => Some(100.0 * self.mass().0),
},
Body::QuadrupedLow(_) => Some(300.0 * self.mass().0),
Body::QuadrupedMedium(_) => Some(300.0 * self.mass().0),
Body::QuadrupedSmall(_) => Some(300.0 * self.mass().0),
Body::Ship(ship) if ship.has_water_thrust() => Some(3500.0 * self.mass().0),
Body::Ship(_) => None,
}
}
/// Returns thrust force if the body type can fly, otherwise None
pub fn fly_thrust(&self) -> Option<f32> {
match self {
Body::BirdMedium(_) => Some(GRAVITY * self.mass().0 * 2.0),
Body::BirdLarge(_) => Some(GRAVITY * self.mass().0 * 0.5),
Body::Dragon(_) => Some(200_000.0),
Body::Ship(ship) if ship.can_fly() => Some(300_000.0),
2021-03-12 14:56:47 +00:00
_ => None,
}
2020-11-03 22:46:07 +00:00
}
/// Returns jump impulse if the body type can jump, otherwise None
pub fn jump_impulse(&self) -> Option<f32> {
2021-03-13 20:05:02 +00:00
match self {
Body::Object(_) | Body::Ship(_) => None,
Body::BipedLarge(_) | Body::Dragon(_) | Body::Golem(_) | Body::QuadrupedLow(_) => {
Some(0.1 * self.mass().0)
},
Body::QuadrupedMedium(_) => Some(0.4 * self.mass().0),
Body::Theropod(body) => match body.species {
theropod::Species::Snowraptor
| theropod::Species::Sandraptor
| theropod::Species::Woodraptor => Some(0.4 * self.mass().0),
_ => None,
},
_ => Some(0.4 * self.mass().0),
2021-03-13 20:05:02 +00:00
}
.map(|f| f * GRAVITY)
2021-03-13 20:05:02 +00:00
}
pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
/// Returns how well a body can move backwards while strafing (0.0 = not at
/// all, 1.0 = same as forward)
2021-06-23 02:08:34 +00:00
pub fn reverse_move_factor(&self) -> f32 { 0.45 }
/// Returns the position where a projectile should be fired relative to this
/// body
pub fn projectile_offsets(&self, ori: Vec3<f32>) -> Vec3<f32> {
let body_offsets_z = match self {
Body::Golem(_) => self.height() * 0.4,
_ => self.eye_height(),
};
let dim = self.dimensions();
// The width (shoulder to shoulder) and length (nose to tail)
let (width, length) = (dim.x, dim.y);
let body_radius = if length > width {
// Dachshund-like
self.max_radius()
} else {
// Cyclops-like
self.min_radius()
};
Vec3::new(
body_radius * ori.x * 1.1,
body_radius * ori.y * 1.1,
body_offsets_z,
)
}
2020-07-03 18:30:41 +00:00
}
2020-03-08 17:38:18 +00:00
/// Handles updating `Components` to move player based on state of `JoinData`
pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
let submersion = data
.physics
.in_liquid()
.map(|depth| depth / data.body.height());
if input_is_pressed(data, InputKind::Fly)
&& submersion.map_or(true, |sub| sub < 1.0)
2021-06-20 03:51:04 +00:00
&& (data.physics.on_ground.is_none() || data.body.jump_impulse().is_none())
&& data.body.fly_thrust().is_some()
{
fly_move(data, update, efficiency);
2021-06-20 03:51:04 +00:00
} else if let Some(submersion) = (data.physics.on_ground.is_none()
&& data.body.swim_thrust().is_some())
.then_some(submersion)
.flatten()
2021-03-14 18:42:39 +00:00
{
swim_move(data, update, efficiency, submersion);
2020-03-07 18:15:02 +00:00
} else {
basic_move(data, update, efficiency);
2020-03-07 18:15:02 +00:00
}
}
2020-03-08 17:38:18 +00:00
/// Updates components to move player as if theyre on ground or in air
fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
2021-06-20 03:51:04 +00:00
let accel = if data.physics.on_ground.is_some() {
data.body.base_accel()
} else {
data.body.air_accel()
} * efficiency;
// Should ability to backpedal be separate from ability to strafe?
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* if data.body.can_strafe() {
data.inputs.move_dir
* if is_strafing(data, update) {
Lerp::lerp(
Vec2::from(update.ori)
.try_normalized()
.unwrap_or_else(Vec2::zero)
2021-06-23 02:08:34 +00:00
.dot(
data.inputs
.move_dir
.try_normalized()
.unwrap_or_else(Vec2::zero),
)
.add(1.0)
.div(2.0)
.max(0.0),
1.0,
data.body.reverse_move_factor(),
)
} else {
1.0
}
} else {
let fw = Vec2::from(update.ori);
fw * data.inputs.move_dir.dot(fw).max(0.0)
};
2020-03-20 22:03:29 +00:00
}
/// Handles forced movement
pub fn handle_forced_movement(
data: &JoinData<'_>,
update: &mut StateUpdate,
movement: ForcedMovement,
) {
match movement {
ForcedMovement::Forward { strength } => {
2021-05-26 02:20:27 +00:00
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
2021-06-20 03:51:04 +00:00
if let Some(accel) = data.physics.on_ground.map(|_| data.body.base_accel()) {
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
2021-05-26 02:20:27 +00:00
* (data.inputs.move_dir + Vec2::from(update.ori))
* strength;
}
},
ForcedMovement::Leap {
vertical,
forward,
progress,
direction,
} => {
let dir = direction.get_2d_dir(data);
// Apply jumping force
update.vel.0 = Vec3::new(
dir.x,
dir.y,
vertical,
)
// Multiply decreasing amount linearly over time (with average of 1)
* 2.0 * progress
// Apply direction
+ Vec3::from(dir)
// Multiply by forward leap strength
* forward
// Control forward movement based on look direction.
// This allows players to stop moving forward when they
// look downward at target
* (1.0 - data.inputs.look_dir.z.abs());
},
ForcedMovement::Hover { move_input } => {
update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0)
+ move_input * data.inputs.move_dir.try_normalized().unwrap_or_default();
},
}
}
pub fn handle_orientation(
data: &JoinData<'_>,
update: &mut StateUpdate,
efficiency: f32,
dir_override: Option<Dir>,
) {
// Direction is set to the override if one is provided, else if entity is
// strafing or attacking the horiontal component of the look direction is used,
// else the current horizontal movement direction is used
let target_ori = if let Some(dir_override) = dir_override {
dir_override.into()
} else if is_strafing(data, update) || update.character.is_attack() {
data.inputs
.look_dir
.to_horizontal()
.unwrap_or_default()
.into()
} else {
Dir::from_unnormalized(data.inputs.move_dir.into())
.map_or_else(|| data.ori.to_horizontal(), |dir| dir.into())
};
let rate = {
// Angle factor used to keep turning rate approximately constant by
// counteracting slerp turning more with a larger angle
let angle_factor = 2.0 / (1.0 - update.ori.dot(target_ori)).sqrt();
data.body.base_ori_rate() * efficiency * angle_factor
};
update.ori = update
.ori
.slerped_towards(target_ori, (data.dt.0 * rate).min(1.0));
}
2020-03-08 17:38:18 +00:00
/// Updates components to move player as if theyre swimming
fn swim_move(
data: &JoinData<'_>,
update: &mut StateUpdate,
efficiency: f32,
submersion: f32,
) -> bool {
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(force) = data.body.swim_thrust() {
let force = efficiency * force;
let mut water_accel = force / data.mass.0;
if let Ok(Some(level)) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
2021-08-22 10:50:01 +00:00
let modifiers = SKILL_MODIFIERS.general_tree.swim;
2021-08-16 17:43:57 +00:00
water_accel *= modifiers.speed.powi(level.into());
}
let dir = if data.body.can_strafe() {
data.inputs.move_dir
} else {
let fw = Vec2::from(update.ori);
fw * data.inputs.move_dir.dot(fw).max(0.0)
};
// Autoswim to stay afloat
let move_z = if submersion < 1.0 && data.inputs.move_z.abs() < std::f32::EPSILON {
(submersion - 0.1).max(0.0)
} else {
data.inputs.move_z
};
update.vel.0 += Vec3::broadcast(data.dt.0)
* Vec3::new(dir.x, dir.y, move_z)
.try_normalized()
.unwrap_or_default()
* water_accel
* (submersion - 0.2).clamp(0.0, 1.0).powi(2);
true
} else {
false
}
2020-11-03 22:46:07 +00:00
}
/// Updates components to move entity as if it's flying
pub fn fly_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) -> bool {
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
2021-04-27 14:41:48 +00:00
let glider = match data.character {
CharacterState::Glide(data) => Some(data),
_ => None,
};
if let Some(force) = data
.body
.fly_thrust()
.or_else(|| glider.is_some().then_some(0.0))
{
let thrust = efficiency * force;
let accel = thrust / data.mass.0;
handle_orientation(data, update, efficiency, None);
2021-04-27 14:41:48 +00:00
// Elevation control
match data.body {
// flappy flappy
2021-04-27 14:41:48 +00:00
Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => {
let anti_grav = GRAVITY * (1.0 + data.inputs.move_z.min(0.0));
update.vel.0.z += data.dt.0 * (anti_grav + accel * data.inputs.move_z.max(0.0));
},
// floaty floaty
Body::Ship(ship) if ship.can_fly() => {
let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
// Reset to default on no input
let change = if data.inputs.move_z.abs() > std::f32::EPSILON {
-data.inputs.move_z
} else {
(def - data.density.0).max(-1.0).min(1.0)
};
Density((update.density.0 + data.dt.0 * rate * change).clamp(min, max))
};
let def_density = ship.density().0;
if data.physics.in_liquid().is_some() {
let hull_density = ship.hull_density().0;
update.density.0 =
regulate_density(def_density * 0.6, hull_density, hull_density, 25.0).0;
} else {
update.density.0 =
regulate_density(def_density * 0.5, def_density * 1.5, def_density, 0.5).0;
};
},
// oopsie woopsie
// TODO: refactor to make this state impossible
_ => {},
};
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* if data.body.can_strafe() {
data.inputs.move_dir
} else {
let fw = Vec2::from(update.ori);
fw * data.inputs.move_dir.dot(fw).max(0.0)
};
2020-11-03 22:46:07 +00:00
true
} else {
false
}
2020-03-07 18:15:02 +00:00
}
/// Checks if an input related to an attack is held. If one is, moves entity
/// into wielding state
pub fn handle_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-03-21 16:09:16 +00:00
if data.controller.queued_inputs.keys().any(|i| i.is_ability()) {
2020-03-08 17:04:26 +00:00
attempt_wield(data, update);
2020-03-07 18:15:02 +00:00
}
}
2020-03-08 17:04:26 +00:00
/// If a tool is equipped, goes into Equipping state, otherwise goes to Idle
pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-05-09 21:18:36 +00:00
// Closure to get equip time provided an equip slot if a tool is equipped in
// equip slot
let equip_time = |equip_slot| {
data.inventory
.and_then(|inv| inv.equipped(equip_slot))
2021-05-09 21:18:36 +00:00
.and_then(|item| match item.kind() {
ItemKind::Tool(tool) => Some((item, tool)),
_ => None,
})
.map(|(item, tool)| tool.equip_time(data.msm, item.components()))
};
2021-05-17 02:35:17 +00:00
// Calculates time required to equip weapons, if weapon in mainhand and offhand,
2021-05-09 21:18:36 +00:00
// uses maximum duration
let mainhand_equip_time = equip_time(EquipSlot::ActiveMainhand);
let offhand_equip_time = equip_time(EquipSlot::ActiveOffhand);
let equip_time = match (mainhand_equip_time, offhand_equip_time) {
(Some(a), Some(b)) => Some(a.max(b)),
(Some(a), None) | (None, Some(a)) => Some(a),
(None, None) => None,
};
// Moves entity into equipping state if there is some equip time, else moves
2021-10-05 00:15:58 +00:00
// instantly into wield
2021-05-09 21:18:36 +00:00
if let Some(equip_time) = equip_time {
2020-03-14 21:17:27 +00:00
update.character = CharacterState::Equipping(equipping::Data {
static_data: equipping::StaticData {
2021-05-09 21:18:36 +00:00
buildup_duration: equip_time,
},
timer: Duration::default(),
2021-10-05 00:15:58 +00:00
is_sneaking: update.character.is_stealthy(),
2020-03-14 21:17:27 +00:00
});
2020-03-08 17:04:26 +00:00
} else {
2021-10-05 00:15:58 +00:00
update.character = CharacterState::Wielding(wielding::Data {
is_sneaking: update.character.is_stealthy(),
});
}
2020-03-08 17:04:26 +00:00
}
2020-03-08 17:38:18 +00:00
/// Checks that player can `Sit` and updates `CharacterState` if so
pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-06-20 03:51:04 +00:00
if data.physics.on_ground.is_some() {
2020-03-21 22:55:20 +00:00
update.character = CharacterState::Sit;
}
}
pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-06-20 03:51:04 +00:00
if data.physics.on_ground.is_some() && data.body.is_humanoid() {
2020-05-27 06:41:55 +00:00
update.character = CharacterState::Dance;
}
}
pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-06-20 03:51:04 +00:00
if data.physics.on_ground.is_some() {
update.character = CharacterState::Talk;
}
}
pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-06-20 03:51:04 +00:00
if data.physics.on_ground.is_some() && data.body.is_humanoid() {
2021-10-05 00:15:58 +00:00
update.character = CharacterState::Idle(idle::Data { is_sneaking: true });
2020-08-02 05:09:11 +00:00
}
}
2020-03-08 17:38:18 +00:00
/// Checks that player can `Climb` and updates `CharacterState` if so
pub fn handle_climb(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
2020-03-24 07:38:16 +00:00
if data.inputs.climb.is_some()
2020-03-07 18:15:02 +00:00
&& data.physics.on_wall.is_some()
2021-06-20 03:51:04 +00:00
&& data.physics.on_ground.is_none()
2020-08-24 01:31:05 +00:00
&& !data
.physics
.in_liquid()
2020-08-24 01:31:05 +00:00
.map(|depth| depth > 1.0)
.unwrap_or(false)
2020-02-03 19:43:36 +00:00
//&& update.vel.0.z < 0.0
&& data.body.can_climb()
&& update.energy.current() > 10.0
{
update.character = CharacterState::Climb(climb::Data::create_adjusted_by_skills(data));
2021-04-27 14:41:48 +00:00
true
} else {
false
}
}
/// Checks that player can Swap Weapons and updates `Loadout` if so
pub fn attempt_swap_equipped_weapons(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-05-09 21:18:36 +00:00
if data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::InactiveMainhand))
2021-05-09 21:18:36 +00:00
.is_some()
|| data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::InactiveOffhand))
2021-05-09 21:18:36 +00:00
.is_some()
{
update.swap_equipped_weapons = true;
2020-03-25 14:58:26 +00:00
}
}
/// Handles inventory manipulations that affect the loadout
pub fn handle_manipulate_loadout(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
update: &mut StateUpdate,
inv_action: InventoryAction,
) {
match inv_action {
InventoryAction::Use(Slot::Inventory(inv_slot)) => {
// If inventory action is using a slot, and slot is in the inventory
// TODO: Do some non lazy way of handling the possibility that items equipped in
// the loadout will have effects that are desired to be non-instantaneous
use use_item::ItemUseKind;
if let Some((item_kind, item)) = data
.inventory
.and_then(|inv| inv.get(inv_slot))
.and_then(|item| Option::<ItemUseKind>::from(item.kind()).zip(Some(item)))
{
let (buildup_duration, use_duration, recover_duration) = item_kind.durations();
// If item returns a valid kind for item use, do into use item character state
update.character = CharacterState::UseItem(use_item::Data {
static_data: use_item::StaticData {
buildup_duration,
use_duration,
recover_duration,
inv_slot,
item_kind,
item_definition_id: item.item_definition_id().to_string(),
2021-10-05 00:15:58 +00:00
was_wielded: matches!(data.character, CharacterState::Wielding(_)),
was_sneak: data.character.is_stealthy(),
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
});
} else {
2021-10-05 00:15:58 +00:00
// Else emit inventory action instantaneously
output_events
.emit_server(ServerEvent::InventoryManip(data.entity, inv_action.into()));
}
},
InventoryAction::Collect(sprite_pos) => {
2021-09-06 15:43:43 +00:00
let sprite_pos_f32 = sprite_pos.map(|x| x as f32 + 0.5);
// Closure to check if distance between a point and the sprite is less than
// MAX_PICKUP_RANGE and the radius of the body
let sprite_range_check = |pos: Vec3<f32>| {
(sprite_pos_f32 - pos).magnitude_squared()
< (MAX_PICKUP_RANGE + data.body.max_radius()).powi(2)
};
// Checks if player's feet or head is near to sprite
let close_to_sprite = sprite_range_check(data.pos.0)
|| sprite_range_check(data.pos.0 + Vec3::new(0.0, 0.0, data.body.height()));
if close_to_sprite {
// First, get sprite data for position, if there is a sprite
use sprite_interact::SpriteInteractKind;
let sprite_at_pos = data
.terrain
.get(sprite_pos)
.ok()
.copied()
.and_then(|b| b.get_sprite());
// Checks if position has a collectible sprite as well as what sprite is at the
// position
let sprite_interact = sprite_at_pos.and_then(Option::<SpriteInteractKind>::from);
if let Some(sprite_interact) = sprite_interact {
// Do a check that a path can be found between sprite and entity
// interacting with sprite Use manhattan distance * 1.5 for number
// of iterations
let iters =
(3.0 * (sprite_pos_f32 - data.pos.0).map(|x| x.abs()).sum()) as usize;
// Heuristic compares manhattan distance of start and end pos
let heuristic =
move |pos: &Vec3<i32>| (sprite_pos - pos).map(|x| x.abs()).sum() as f32;
let mut astar = Astar::new(
iters,
data.pos.0.map(|x| x.floor() as i32),
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
// Neighbors are all neighboring blocks that are air
let neighbors = |pos: &Vec3<i32>| {
const DIRS: [Vec3<i32>; 6] = [
Vec3::new(1, 0, 0),
Vec3::new(-1, 0, 0),
Vec3::new(0, 1, 0),
Vec3::new(0, -1, 0),
Vec3::new(0, 0, 1),
Vec3::new(0, 0, -1),
];
let pos = *pos;
DIRS.iter().map(move |dir| dir + pos).filter(|pos| {
data.terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
2021-09-06 15:43:43 +00:00
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
};
// Pathing satisfied when it reaches the sprite position
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
let not_blocked_by_terrain = astar
.poll(iters, heuristic, neighbors, transition, satisfied)
.into_path()
.is_some();
// If path can be found between entity interacting with sprite and entity, start
// interaction with sprite
if not_blocked_by_terrain {
// If the sprite is collectible, enter the sprite interaction character
// state TODO: Handle cases for sprite being
// interactible, but not collectible (none currently
// exist)
let (buildup_duration, use_duration, recover_duration) =
sprite_interact.durations();
update.character = CharacterState::SpriteInteract(sprite_interact::Data {
static_data: sprite_interact::StaticData {
buildup_duration,
use_duration,
recover_duration,
sprite_pos,
sprite_kind: sprite_interact,
2021-10-05 00:15:58 +00:00
was_wielded: matches!(data.character, CharacterState::Wielding(_)),
was_sneak: data.character.is_stealthy(),
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
})
}
}
}
},
_ => {
// Else just do event instantaneously
output_events.emit_server(ServerEvent::InventoryManip(data.entity, inv_action.into()));
},
}
}
/// Checks that player can wield the glider and updates `CharacterState` if so
pub fn attempt_glide_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
if data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::Glider))
.is_some()
2020-08-12 14:10:12 +00:00
&& !data
.physics
.in_liquid()
2020-08-12 14:10:12 +00:00
.map(|depth| depth > 1.0)
.unwrap_or(false)
&& data.body.is_humanoid()
{
update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
2020-02-03 10:54:50 +00:00
}
}
2020-03-08 17:38:18 +00:00
/// Checks that player can jump and sends jump event if so
pub fn handle_jump(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
// TODO: remove?
_update: &mut StateUpdate,
strength: f32,
) -> bool {
2021-06-20 03:51:04 +00:00
(input_is_pressed(data, InputKind::Jump) && data.physics.on_ground.is_some())
.then(|| data.body.jump_impulse())
.flatten()
.map(|impulse| {
output_events.emit_local(LocalEvent::Jump(
data.entity,
strength * impulse / data.mass.0 * data.stats.move_speed_modifier,
));
})
.is_some()
2020-02-03 10:54:50 +00:00
}
fn handle_ability(data: &JoinData<'_>, update: &mut StateUpdate, input: InputKind) {
2021-11-11 22:55:14 +00:00
if let Some(ability_input) = input.into() {
if let Some((ability, from_offhand)) = data
2021-11-12 03:37:37 +00:00
.active_abilities
.activate_ability(
ability_input,
data.inventory,
data.skill_set,
Some(data.body),
)
2021-11-11 22:55:14 +00:00
.filter(|(ability, _)| ability.requirements_paid(data, update))
{
update.character = CharacterState::from((
&ability,
AbilityInfo::from_input(data, from_offhand, input),
data,
));
}
2020-02-03 10:54:50 +00:00
}
}
pub fn handle_ability_input(data: &JoinData<'_>, update: &mut StateUpdate) {
if let Some(input) = data
.controller
.queued_inputs
2021-03-21 19:11:44 +00:00
.keys()
.find(|i| i.is_ability())
{
handle_ability(data, update, *input);
2020-03-24 12:59:53 +00:00
}
}
pub fn handle_input(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
update: &mut StateUpdate,
input: InputKind,
) {
match input {
2021-03-14 18:42:39 +00:00
InputKind::Primary | InputKind::Secondary | InputKind::Ability(_) => {
handle_ability(data, update, input)
2021-03-13 00:38:20 +00:00
},
2021-03-14 18:42:39 +00:00
InputKind::Roll => handle_dodge_input(data, update),
InputKind::Jump => {
handle_jump(data, output_events, update, 1.0);
},
2021-04-10 03:40:20 +00:00
InputKind::Block => handle_block_input(data, update),
2021-03-14 18:42:39 +00:00
InputKind::Fly => {},
2020-02-24 14:35:07 +00:00
}
}
pub fn attempt_input(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
update: &mut StateUpdate,
) {
// TODO: look into using first() when it becomes stable
2021-03-21 19:11:44 +00:00
if let Some(input) = data.controller.queued_inputs.keys().next() {
handle_input(data, output_events, update, *input);
}
}
2021-04-10 03:40:20 +00:00
/// Checks that player can block, then attempts to block
pub fn handle_block_input(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-04-10 03:40:20 +00:00
let can_block =
|equip_slot| matches!(unwrap_tool_data(data, equip_slot), Some(tool) if tool.can_block());
let hands = get_hands(data);
if input_is_pressed(data, InputKind::Block)
2021-05-09 21:18:36 +00:00
&& (can_block(EquipSlot::ActiveMainhand)
|| (hands.0.is_none() && can_block(EquipSlot::ActiveOffhand)))
{
2021-04-10 03:40:20 +00:00
let ability = CharacterAbility::default_block();
if ability.requirements_paid(data, update) {
update.character = CharacterState::from((
&ability,
AbilityInfo::from_input(data, false, InputKind::Roll),
data,
2021-04-10 03:40:20 +00:00
));
}
}
}
2020-03-08 17:38:18 +00:00
/// Checks that player can perform a dodge, then
/// attempts to perform their dodge ability
pub fn handle_dodge_input(data: &JoinData<'_>, update: &mut StateUpdate) {
2021-03-14 18:42:39 +00:00
if input_is_pressed(data, InputKind::Roll) && data.body.is_humanoid() {
2021-07-11 18:41:52 +00:00
let ability = CharacterAbility::default_roll().adjusted_by_skills(data.skill_set, None);
if ability.requirements_paid(data, update) {
update.character = CharacterState::from((
&ability,
AbilityInfo::from_input(data, false, InputKind::Roll),
data,
));
2021-10-05 00:15:58 +00:00
if let CharacterState::Roll(roll) = &mut update.character {
if let CharacterState::ComboMelee(c) = data.character {
2021-03-14 18:42:39 +00:00
roll.was_combo = Some((c.static_data.ability_info.input, c.stage));
roll.was_wielded = true;
2021-10-05 00:15:58 +00:00
} else {
if data.character.is_wield() {
roll.was_wielded = true;
}
if data.character.is_stealthy() {
roll.is_sneaking = true;
}
}
2020-03-26 13:46:08 +00:00
}
2020-02-03 10:54:50 +00:00
}
}
2019-12-28 16:10:39 +00:00
}
2020-03-07 18:15:02 +00:00
pub fn is_strafing(data: &JoinData<'_>, update: &StateUpdate) -> bool {
// TODO: Don't always check `character.is_aimed()`, allow the frontend to
// control whether the player strafes during an aimed `CharacterState`.
(update.character.is_aimed() || update.should_strafe) && data.body.can_strafe()
}
pub fn unwrap_tool_data<'a>(data: &'a JoinData, equip_slot: EquipSlot) -> Option<&'a Tool> {
if let Some(ItemKind::Tool(tool)) = data
.inventory
.and_then(|inv| inv.equipped(equip_slot))
.map(|i| i.kind())
{
2021-07-11 18:41:52 +00:00
Some(tool)
} else {
None
}
}
pub fn get_hands(data: &JoinData<'_>) -> (Option<Hands>, Option<Hands>) {
let hand = |slot| {
if let Some(ItemKind::Tool(tool)) = data
.inventory
.and_then(|inv| inv.equipped(slot))
.map(|i| i.kind())
{
Some(tool.hands)
} else {
None
}
};
2021-05-09 21:18:36 +00:00
(
hand(EquipSlot::ActiveMainhand),
hand(EquipSlot::ActiveOffhand),
)
}
/// Returns (critical chance, critical multiplier) which is calculated from
/// equipped weapon and equipped armor respectively
pub fn get_crit_data(data: &JoinData<'_>, ai: AbilityInfo) -> (f32, f32) {
const DEFAULT_CRIT_CHANCE: f32 = 0.1;
let crit_chance = ai
.hand
.map(|hand| match hand {
HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
HandInfo::OffHand => EquipSlot::ActiveOffhand,
})
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
.and_then(|item| {
if let ItemKind::Tool(tool) = item.kind() {
Some(tool.base_crit_chance(data.msm, item.components()))
} else {
None
}
})
.unwrap_or(DEFAULT_CRIT_CHANCE);
let crit_mult = combat::compute_crit_mult(data.inventory);
(crit_chance, crit_mult)
}
2021-07-10 16:04:12 +00:00
/// Returns buff strength from the weapon used in the ability
pub fn get_buff_strength(data: &JoinData<'_>, ai: AbilityInfo) -> f32 {
2021-07-10 16:04:12 +00:00
ai.hand
.map(|hand| match hand {
HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
HandInfo::OffHand => EquipSlot::ActiveOffhand,
})
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
2021-07-10 16:04:12 +00:00
.and_then(|item| {
if let ItemKind::Tool(tool) = item.kind() {
Some(tool.base_buff_strength(data.msm, item.components()))
} else {
None
}
})
.unwrap_or(1.0)
}
pub fn handle_state_interrupt(
data: &JoinData<'_>,
update: &mut StateUpdate,
attacks_interrupt: bool,
) {
if attacks_interrupt {
handle_ability_input(data, update);
2020-11-20 17:30:48 +00:00
}
handle_dodge_input(data, update);
}
pub fn input_is_pressed(data: &JoinData<'_>, input: InputKind) -> bool {
2021-03-21 16:09:16 +00:00
data.controller.queued_inputs.contains_key(&input)
}
2021-05-30 19:39:00 +00:00
/// Checked `Duration` addition. Computes `timer` + `dt`, applying relevant stat
/// attack modifiers and `other_modifiers`, returning None if overflow occurred.
pub fn checked_tick_attack(
data: &JoinData<'_>,
2021-05-30 19:39:00 +00:00
timer: Duration,
other_modifier: Option<f32>,
) -> Option<Duration> {
2021-05-30 20:36:18 +00:00
timer.checked_add(Duration::from_secs_f32(
data.dt.0 * data.stats.attack_speed_modifier * other_modifier.unwrap_or(1.0),
))
2021-05-30 19:39:00 +00:00
}
/// Ticks `timer` by `dt`, applying relevant stat attack modifiers and
/// `other_modifier`. Returns `Duration::default()` if overflow occurs
pub fn tick_attack_or_default(
data: &JoinData<'_>,
2021-05-30 19:39:00 +00:00
timer: Duration,
other_modifier: Option<f32>,
) -> Duration {
checked_tick_attack(data, timer, other_modifier).unwrap_or_default()
}
/// Determines what portion a state is in. Used in all attacks (eventually). Is
/// used to control aspects of animation code, as well as logic within the
/// character states.
#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum StageSection {
Buildup,
Recover,
Charge,
2020-09-28 02:38:23 +00:00
Movement,
Action,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum ForcedMovement {
Forward {
strength: f32,
},
Leap {
vertical: f32,
forward: f32,
progress: f32,
direction: MovementDirection,
},
Hover {
move_input: f32,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum MovementDirection {
Look,
Move,
}
impl MovementDirection {
pub fn get_2d_dir(self, data: &JoinData<'_>) -> Vec2<f32> {
use MovementDirection::*;
match self {
Look => data
.inputs
.look_dir
.to_horizontal()
.unwrap_or_default()
.xy(),
Move => data.inputs.move_dir,
}
.try_normalized()
.unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct AbilityInfo {
pub tool: Option<ToolKind>,
pub hand: Option<HandInfo>,
2021-03-14 18:42:39 +00:00
pub input: InputKind,
pub input_attr: Option<InputAttr>,
}
impl AbilityInfo {
pub fn from_input(data: &JoinData<'_>, from_offhand: bool, input: InputKind) -> Self {
let tool_data = if from_offhand {
2021-05-09 21:18:36 +00:00
unwrap_tool_data(data, EquipSlot::ActiveOffhand)
} else {
2021-05-09 21:18:36 +00:00
unwrap_tool_data(data, EquipSlot::ActiveMainhand)
};
let (tool, hand) = (
tool_data.map(|t| t.kind),
tool_data.map(|t| HandInfo::from_main_tool(t, from_offhand)),
);
Self {
2021-03-21 16:09:16 +00:00
tool,
hand,
input,
input_attr: data.controller.queued_inputs.get(&input).copied(),
}
2021-02-15 05:07:00 +00:00
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum HandInfo {
TwoHanded,
MainHand,
OffHand,
}
impl HandInfo {
pub fn from_main_tool(tool: &Tool, from_offhand: bool) -> Self {
match tool.hands {
Hands::Two => Self::TwoHanded,
Hands::One => {
if from_offhand {
Self::OffHand
} else {
Self::MainHand
}
},
}
}
}