use crate::{
astar::Astar,
comp::{
ability::{AbilityInitEvent, AbilityMeta, Capability, SpecifiedAbility, Stance},
arthropod, biped_large, biped_small, bird_medium,
buff::{Buff, BuffCategory, BuffChange, BuffData, BuffSource, DestInfo},
character_state::OutputEvents,
controller::InventoryManip,
crustacean, golem,
inventory::slot::{ArmorSlot, EquipSlot, Slot},
item::{
armor::Friction,
tool::{self, AbilityContext},
Hands, ItemKind, ToolKind,
},
quadruped_low, quadruped_medium, quadruped_small, ship,
skills::{Skill, SwimSkill, SKILL_MODIFIERS},
theropod, Alignment, Body, CharacterState, Density, InputAttr, InputKind, InventoryAction,
Melee, Pos, StateUpdate,
},
consts::{FRIC_GROUND, GRAVITY, MAX_MOUNT_RANGE, MAX_PICKUP_RANGE},
event::{BuffEvent, ChangeStanceEvent, ComboChangeEvent, InventoryManipEvent, LocalEvent},
mounting::Volume,
outcome::Outcome,
states::{behavior::JoinData, utils::CharacterState::Idle, *},
terrain::{Block, TerrainGrid, UnlockKind},
uid::Uid,
util::Dir,
vol::ReadVol,
};
use core::hash::BuildHasherDefault;
use fxhash::FxHasher64;
use serde::{Deserialize, Serialize};
use std::{
f32::consts::PI,
ops::{Add, Div, Mul},
time::Duration,
};
use strum::Display;
use vek::*;
pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
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::Truffler => 70.0,
quadruped_small::Species::Fungome => 70.0,
quadruped_small::Species::Goat => 80.0,
quadruped_small::Species::Raccoon => 100.0,
quadruped_small::Species::Frog => 150.0,
quadruped_small::Species::Porcupine => 100.0,
quadruped_small::Species::Beaver => 100.0,
quadruped_small::Species::Rabbit => 110.0,
quadruped_small::Species::Cat => 150.0,
quadruped_small::Species::Quokka => 100.0,
quadruped_small::Species::MossySnail => 20.0,
_ => 125.0,
},
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Grolgar => 100.0,
quadruped_medium::Species::Saber => 110.0,
quadruped_medium::Species::Tiger => 110.0,
quadruped_medium::Species::Tuskram => 85.0,
quadruped_medium::Species::Lion => 105.0,
quadruped_medium::Species::Tarasque => 100.0,
quadruped_medium::Species::Wolf => 115.0,
quadruped_medium::Species::Frostfang => 115.0,
quadruped_medium::Species::Mouflon => 75.0,
quadruped_medium::Species::Catoblepas => 60.0,
quadruped_medium::Species::Bonerattler => 115.0,
quadruped_medium::Species::Deer => 120.0,
quadruped_medium::Species::Hirdrasil => 110.0,
quadruped_medium::Species::Roshwalr => 70.0,
quadruped_medium::Species::Donkey => 90.0,
quadruped_medium::Species::Camel => 75.0,
quadruped_medium::Species::Zebra => 150.0,
quadruped_medium::Species::Antelope => 155.0,
quadruped_medium::Species::Kelpie => 140.0,
quadruped_medium::Species::Horse => 140.0,
quadruped_medium::Species::Barghest => 80.0,
quadruped_medium::Species::Cattle => 80.0,
quadruped_medium::Species::Darkhound => 115.0,
quadruped_medium::Species::Highland => 80.0,
quadruped_medium::Species::Yak => 80.0,
quadruped_medium::Species::Panda => 90.0,
quadruped_medium::Species::Bear => 90.0,
quadruped_medium::Species::Dreadhorn => 95.0,
quadruped_medium::Species::Moose => 105.0,
quadruped_medium::Species::Snowleopard => 115.0,
quadruped_medium::Species::Mammoth => 75.0,
quadruped_medium::Species::Ngoubou => 95.0,
quadruped_medium::Species::Llama => 100.0,
quadruped_medium::Species::Alpaca => 100.0,
quadruped_medium::Species::Akhlut => 90.0,
quadruped_medium::Species::Bristleback => 105.0,
quadruped_medium::Species::ClaySteed => 85.0,
},
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,
biped_large::Species::Minotaur => 60.0,
biped_large::Species::Huskbrute => 130.0,
biped_large::Species::Cultistwarlord => 110.0,
biped_large::Species::Cultistwarlock => 90.0,
biped_large::Species::Gigasfrost => 45.0,
biped_large::Species::Forgemaster => 100.0,
_ => 80.0,
},
Body::BirdMedium(_) => 80.0,
Body::FishMedium(_) => 80.0,
Body::Dragon(_) => 250.0,
Body::BirdLarge(_) => 110.0,
Body::FishSmall(_) => 60.0,
Body::BipedSmall(biped_small) => match biped_small.species {
biped_small::Species::Haniwa => 65.0,
biped_small::Species::Boreal => 100.0,
biped_small::Species::Gnarling => 70.0,
_ => 80.0,
},
Body::Object(_) => 0.0,
Body::ItemDrop(_) => 0.0,
Body::Golem(body) => match body.species {
golem::Species::ClayGolem => 120.0,
golem::Species::IronGolem => 100.0,
_ => 60.0,
},
Body::Theropod(theropod) => match theropod.species {
theropod::Species::Archaeos
| theropod::Species::Odonto
| theropod::Species::Ntouka => 110.0,
theropod::Species::Dodarock => 75.0,
theropod::Species::Yale => 115.0,
_ => 125.0,
},
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
quadruped_low::Species::Crocodile => 60.0,
quadruped_low::Species::SeaCrocodile => 60.0,
quadruped_low::Species::Alligator => 65.0,
quadruped_low::Species::Salamander => 85.0,
quadruped_low::Species::Elbst => 85.0,
quadruped_low::Species::Monitor => 130.0,
quadruped_low::Species::Asp => 100.0,
quadruped_low::Species::Tortoise => 60.0,
quadruped_low::Species::Rocksnapper => 70.0,
quadruped_low::Species::Rootsnapper => 70.0,
quadruped_low::Species::Reefsnapper => 70.0,
quadruped_low::Species::Pangolin => 90.0,
quadruped_low::Species::Maneater => 80.0,
quadruped_low::Species::Sandshark => 125.0,
quadruped_low::Species::Hakulaq => 125.0,
quadruped_low::Species::Dagon => 140.0,
quadruped_low::Species::Lavadrake => 100.0,
quadruped_low::Species::Icedrake => 100.0,
quadruped_low::Species::Basilisk => 85.0,
quadruped_low::Species::Deadwood => 110.0,
quadruped_low::Species::Mossdrake => 100.0,
quadruped_low::Species::Driggle => 120.0,
quadruped_low::Species::Snaretongue => 120.0,
quadruped_low::Species::Hydra => 100.0,
},
Body::Ship(ship::Body::Carriage) => 40.0,
Body::Ship(_) => 0.0,
Body::Arthropod(arthropod) => match arthropod.species {
arthropod::Species::Tarantula => 85.0,
arthropod::Species::Blackwidow => 95.0,
arthropod::Species::Antlion => 115.0,
arthropod::Species::Hornbeetle => 80.0,
arthropod::Species::Leafbeetle => 65.0,
arthropod::Species::Stagbeetle => 80.0,
arthropod::Species::Weevil => 70.0,
arthropod::Species::Cavespider => 90.0,
arthropod::Species::Moltencrawler => 70.0,
arthropod::Species::Mosscrawler => 70.0,
arthropod::Species::Sandcrawler => 70.0,
arthropod::Species::Dagonite => 70.0,
arthropod::Species::Emberfly => 75.0,
},
Body::Crustacean(body) => match body.species {
crustacean::Species::Crab | crustacean::Species::SoldierCrab => 80.0,
crustacean::Species::Karkatha => 120.0,
},
Body::Plugin(body) => body.base_accel(),
}
}
pub fn air_accel(&self) -> f32 { self.base_accel() * 0.025 }
pub fn max_speed_approx(&self) -> f32 {
let v = match self {
Body::Ship(ship) => ship.get_speed(),
_ => (-self.base_accel() / 30.0) / ((1.0 - FRIC_GROUND).powi(2) - 1.0),
};
debug_assert!(v >= 0.0, "Speed must be positive!");
v
}
pub fn base_ori_rate(&self) -> f32 {
match self {
Body::Humanoid(_) => 3.5,
Body::QuadrupedSmall(_) => 3.0,
Body::QuadrupedMedium(quadruped_medium) => match quadruped_medium.species {
quadruped_medium::Species::Mammoth => 1.0,
_ => 2.8,
},
Body::BirdMedium(_) => 6.0,
Body::FishMedium(_) => 6.0,
Body::Dragon(_) => 1.0,
Body::BirdLarge(_) => 7.0,
Body::FishSmall(_) => 7.0,
Body::BipedLarge(biped_large) => match biped_large.species {
biped_large::Species::Harvester => 2.0,
_ => 2.7,
},
Body::BipedSmall(_) => 3.5,
Body::Object(_) => 2.0,
Body::ItemDrop(_) => 2.0,
Body::Golem(golem) => match golem.species {
golem::Species::WoodGolem => 1.2,
_ => 2.0,
},
Body::Theropod(theropod) => match theropod.species {
theropod::Species::Archaeos => 2.3,
theropod::Species::Odonto => 2.3,
theropod::Species::Ntouka => 2.3,
theropod::Species::Dodarock => 2.0,
_ => 2.5,
},
Body::QuadrupedLow(quadruped_low) => match quadruped_low.species {
quadruped_low::Species::Asp => 2.2,
quadruped_low::Species::Tortoise => 1.5,
quadruped_low::Species::Rocksnapper => 1.8,
quadruped_low::Species::Rootsnapper => 1.8,
quadruped_low::Species::Lavadrake => 1.7,
quadruped_low::Species::Icedrake => 1.7,
quadruped_low::Species::Mossdrake => 1.7,
_ => 2.0,
},
Body::Ship(ship::Body::Carriage) => 0.04,
Body::Ship(ship) if ship.has_water_thrust() => 5.0 / self.dimensions().y,
Body::Ship(_) => 6.0 / self.dimensions().y,
Body::Arthropod(_) => 3.5,
Body::Crustacean(_) => 3.5,
Body::Plugin(body) => body.base_ori_rate(),
}
}
pub fn swim_thrust(&self) -> Option<f32> {
let front_profile = self.dimensions().x * self.dimensions().z;
Some(
match self {
Body::Object(_) => return None,
Body::ItemDrop(_) => return None,
Body::Ship(ship::Body::Submarine) => 1000.0 * self.mass().0,
Body::Ship(ship) if ship.has_water_thrust() => 500.0 * self.mass().0,
Body::Ship(_) => return None,
Body::BipedLarge(_) => 120.0 * self.mass().0,
Body::Golem(_) => 100.0 * self.mass().0,
Body::BipedSmall(_) => 1000.0 * self.mass().0,
Body::BirdMedium(_) => 400.0 * self.mass().0,
Body::BirdLarge(_) => 400.0 * self.mass().0,
Body::FishMedium(_) => 200.0 * self.mass().0,
Body::FishSmall(_) => 300.0 * self.mass().0,
Body::Dragon(_) => 50.0 * self.mass().0,
Body::Humanoid(_) => 4_000_000.0 / self.mass().0,
Body::Theropod(body) => match body.species {
theropod::Species::Sandraptor
| theropod::Species::Snowraptor
| theropod::Species::Sunlizard
| theropod::Species::Woodraptor
| theropod::Species::Dodarock
| theropod::Species::Axebeak
| theropod::Species::Yale => 500.0 * self.mass().0,
_ => 150.0 * self.mass().0,
},
Body::QuadrupedLow(_) => 1200.0 * self.mass().0,
Body::QuadrupedMedium(body) => match body.species {
quadruped_medium::Species::Mammoth => 150.0 * self.mass().0,
_ => 1000.0 * self.mass().0,
},
Body::QuadrupedSmall(_) => 1500.0 * self.mass().0,
Body::Arthropod(_) => 500.0 * self.mass().0,
Body::Crustacean(_) => 400.0 * self.mass().0,
Body::Plugin(body) => body.swim_thrust()?,
} * front_profile,
)
}
pub fn fly_thrust(&self) -> Option<f32> {
match self {
Body::BirdMedium(body) => match body.species {
bird_medium::Species::Bat | bird_medium::Species::BloodmoonBat => {
Some(GRAVITY * self.mass().0 * 0.5)
},
_ => 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),
_ => None,
}
}
pub fn jump_impulse(&self) -> Option<f32> {
match self {
Body::Object(_) | Body::Ship(_) | Body::ItemDrop(_) => None,
Body::BipedLarge(_) | Body::Dragon(_) => Some(0.6 * self.mass().0),
Body::Golem(_) | Body::QuadrupedLow(_) => Some(0.4 * 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,
},
Body::Arthropod(_) => Some(1.0 * self.mass().0),
_ => Some(0.4 * self.mass().0),
}
.map(|f| f * GRAVITY)
}
pub fn can_climb(&self) -> bool { matches!(self, Body::Humanoid(_)) }
pub fn reverse_move_factor(&self) -> f32 { 0.45 }
pub fn projectile_offsets(&self, ori: Vec3<f32>, scale: f32) -> Vec3<f32> {
let body_offsets_z = match self {
Body::Golem(_) => self.height() * 0.4,
_ => self.eye_height(scale),
};
let dim = self.dimensions();
let (width, length) = (dim.x, dim.y);
let body_radius = if length > width {
self.max_radius()
} else {
self.min_radius()
};
Vec3::new(
body_radius * ori.x * 1.1,
body_radius * ori.y * 1.1,
body_offsets_z,
)
}
}
pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
if let Idle(idle::Data {
is_sneaking,
time_entered,
mut footwear,
}) = data.character
{
if footwear.is_none() {
footwear = data.inventory.and_then(|inv| {
inv.equipped(EquipSlot::Armor(ArmorSlot::Feet))
.map(|armor| match armor.kind().as_ref() {
ItemKind::Armor(a) => {
a.stats(data.msm, armor.stats_durability_multiplier())
.ground_contact
},
_ => Friction::Normal,
})
});
update.character = Idle(idle::Data {
is_sneaking: *is_sneaking,
time_entered: *time_entered,
footwear,
});
}
if data.physics.skating_active {
update.character =
CharacterState::Skate(skate::Data::new(data, footwear.unwrap_or(Friction::Normal)));
}
}
}
pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
if data.volume_mount_data.is_some() {
return;
}
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)
&& (data.physics.on_ground.is_none() || data.body.jump_impulse().is_none())
&& data.body.fly_thrust().is_some()
{
fly_move(data, update, efficiency);
} else if let Some(submersion) = (data.physics.in_liquid().is_some()
&& data.body.swim_thrust().is_some())
.then_some(submersion)
.flatten()
{
swim_move(data, update, efficiency, submersion);
} else {
basic_move(data, update, efficiency);
}
}
fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
let accel = if let Some(block) = data.physics.on_ground {
data.body.base_accel()
* data.scale.map_or(1.0, |s| s.0.sqrt())
* block.get_traction()
* block.get_friction()
/ FRIC_GROUND
} else {
data.body.air_accel()
} * efficiency;
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)
.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)
};
}
pub fn handle_forced_movement(
data: &JoinData<'_>,
update: &mut StateUpdate,
movement: ForcedMovement,
) {
match movement {
ForcedMovement::Forward(strength) => {
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(accel) = data.physics.on_ground.map(|block| {
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* data.scale.map_or(1.0, |s| s.0.sqrt())
* Vec2::from(*data.ori)
* strength;
}
},
ForcedMovement::Reverse(strength) => {
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(accel) = data.physics.on_ground.map(|block| {
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* data.scale.map_or(1.0, |s| s.0.sqrt())
* -Vec2::from(*data.ori)
* strength;
}
},
ForcedMovement::Sideways(strength) => {
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(accel) = data.physics.on_ground.map(|block| {
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
let direction = {
let side = Vec2::from(*data.ori)
.rotated_z(PI / 2.)
.dot(data.inputs.move_dir)
.signum();
if side > 0.0 {
Vec2::from(*data.ori).rotated_z(PI / 2.)
} else {
-Vec2::from(*data.ori).rotated_z(PI / 2.)
}
};
update.vel.0 += Vec2::broadcast(data.dt.0)
* accel
* data.scale.map_or(1.0, |s| s.0.sqrt())
* direction
* strength;
}
},
ForcedMovement::DirectedReverse(strength) => {
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(accel) = data.physics.on_ground.map(|block| {
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() > 0.0 {
data.inputs.move_dir.reflected(Vec2::from(*data.ori))
} else {
data.inputs.move_dir
}
.try_normalized()
.unwrap_or_else(|| -Vec2::from(*data.ori));
update.vel.0 += direction * strength * accel * data.dt.0;
}
},
ForcedMovement::AntiDirectedForward(strength) => {
let strength = strength * data.stats.move_speed_modifier * data.stats.friction_modifier;
if let Some(accel) = data.physics.on_ground.map(|block| {
data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND
}) {
let direction = if Vec2::from(*data.ori).dot(data.inputs.move_dir).signum() < 0.0 {
data.inputs.move_dir.reflected(Vec2::from(*data.ori))
} else {
data.inputs.move_dir
}
.try_normalized()
.unwrap_or_else(|| Vec2::from(*data.ori));
let direction = direction.reflected(Vec2::from(*data.ori).rotated_z(PI / 2.));
update.vel.0 += direction * strength * accel * data.dt.0;
}
},
ForcedMovement::Leap {
vertical,
forward,
progress,
direction,
} => {
let dir = direction.get_2d_dir(data);
update.vel.0 = Vec3::new(
dir.x,
dir.y,
vertical,
)
* data.scale.map_or(1.0, |s| s.0.sqrt())
* 2.0 * progress
+ Vec3::from(dir)
* forward
* (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.scale.map_or(1.0, |s| s.0.sqrt())
* data.inputs.move_dir.try_normalized().unwrap_or_default();
},
}
}
pub fn handle_orientation(
data: &JoinData<'_>,
update: &mut StateUpdate,
efficiency: f32,
dir_override: Option<Dir>,
) {
fn to_horizontal_fast(ori: &crate::comp::Ori) -> crate::comp::Ori {
if ori.to_quat().into_vec4().xy().is_approx_zero() {
*ori
} else {
ori.to_horizontal()
}
}
fn ori_absdiff(a: &crate::comp::Ori, b: &crate::comp::Ori) -> f32 {
(a.to_quat().into_vec4() - b.to_quat().into_vec4()).reduce(|a, b| a.abs() + b.abs())
}
let (tilt_ori, efficiency) = if let Body::Ship(ship) = data.body
&& ship.has_wheels()
{
let height_at = |rpos| {
data.terrain
.ray(
data.pos.0 + rpos + Vec3::unit_z() * 4.0,
data.pos.0 + rpos - Vec3::unit_z() * 4.0,
)
.until(Block::is_solid)
.cast()
.0
};
let x_diff = (height_at(data.ori.to_horizontal().right().to_vec() * 3.0)
- height_at(data.ori.to_horizontal().right().to_vec() * -3.0))
/ 10.0;
let y_diff = (height_at(data.ori.to_horizontal().look_dir().to_vec() * -4.5)
- height_at(data.ori.to_horizontal().look_dir().to_vec() * 4.5))
/ 10.0;
(
Quaternion::rotation_y(x_diff.atan()) * Quaternion::rotation_x(y_diff.atan()),
(data.vel.0 - data.physics.ground_vel)
.xy()
.magnitude()
.max(3.0)
* efficiency,
)
} else {
(Quaternion::identity(), efficiency)
};
let target_ori = if let Some(dir_override) = dir_override {
dir_override.into()
} else if is_strafing(data, update) || update.character.should_follow_look() {
data.inputs
.look_dir
.to_horizontal()
.unwrap_or_default()
.into()
} else {
Dir::from_unnormalized(data.inputs.move_dir.into())
.map_or_else(|| to_horizontal_fast(data.ori), |dir| dir.into())
}
.rotated(tilt_ori);
let half_turns_per_tick = data.body.base_ori_rate() / data.scale.map_or(1.0, |s| s.0.sqrt())
* efficiency
* if data.physics.on_ground.is_some() {
1.0
} else if data.physics.in_liquid().is_some() {
0.4
} else {
0.2
}
* data.dt.0;
let ticks_from_target_guess = ori_absdiff(&update.ori, &target_ori) / half_turns_per_tick;
let instantaneous = ticks_from_target_guess < 1.0;
update.ori = if data.volume_mount_data.is_some() {
update.ori
} else if instantaneous {
target_ori
} else {
let target_fraction = {
let angle_factor = 2.0 / (1.0 - update.ori.dot(target_ori)).sqrt();
half_turns_per_tick * angle_factor
};
update
.ori
.slerped_towards(target_ori, target_fraction.min(1.0))
};
update.character_activity.look_dir = Some(data.controller.inputs.look_dir);
}
fn swim_move(
data: &JoinData<'_>,
update: &mut StateUpdate,
efficiency: f32,
submersion: f32,
) -> bool {
let efficiency = efficiency * data.stats.swim_speed_modifier * data.stats.friction_modifier;
if let Some(force) = data.body.swim_thrust() {
let force = efficiency * force * data.scale.map_or(1.0, |s| s.0);
let mut water_accel = force / data.mass.0;
if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) {
let modifiers = SKILL_MODIFIERS.general_tree.swim;
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)
};
let move_z = if submersion < 1.0
&& data.inputs.move_z.abs() < f32::EPSILON
&& data.physics.on_ground.is_none()
{
submersion.max(0.0) * 0.1
} else {
data.inputs.move_z
};
let move_z = move_z.min((submersion * 1.5 - 0.5).clamp(0.0, 1.0).powi(2));
update.vel.0 += Vec3::new(dir.x, dir.y, move_z)
* water_accel
* submersion.clamp(0.0, 1.0).sqrt()
* data.dt.0 * 0.04;
true
} else {
false
}
}
pub fn fly_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) -> bool {
let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier;
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);
match data.body {
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));
},
Body::Ship(ship) if ship.can_fly() => {
let regulate_density = |min: f32, max: f32, def: f32, rate: f32| -> Density {
let change = if data.inputs.move_z.abs() > f32::EPSILON {
-data.inputs.move_z
} else {
(def - data.density.0).clamp(-1.0, 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;
};
},
_ => {},
};
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)
};
true
} else {
false
}
}
pub fn handle_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
if data.controller.queued_inputs.keys().any(|i| i.is_ability()) {
attempt_wield(data, update);
}
}
pub fn attempt_wield(data: &JoinData<'_>, update: &mut StateUpdate) {
let equip_time = |equip_slot| {
data.inventory
.and_then(|inv| inv.equipped(equip_slot))
.and_then(|item| match &*item.kind() {
ItemKind::Tool(tool) => Some(Duration::from_secs_f32(
tool.stats(item.stats_durability_multiplier())
.equip_time_secs,
)),
_ => None,
})
};
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,
};
if let Some(equip_time) = equip_time {
update.character = CharacterState::Equipping(equipping::Data {
static_data: equipping::StaticData {
buildup_duration: equip_time,
},
timer: Duration::default(),
is_sneaking: update.character.is_stealthy(),
});
} else {
update.character = CharacterState::Wielding(wielding::Data {
is_sneaking: update.character.is_stealthy(),
});
}
}
pub fn attempt_sit(data: &JoinData<'_>, update: &mut StateUpdate) {
if data.physics.on_ground.is_some() {
update.character = CharacterState::Sit;
}
}
pub fn attempt_dance(data: &JoinData<'_>, update: &mut StateUpdate) {
if data.physics.on_ground.is_some() && data.body.is_humanoid() {
update.character = CharacterState::Dance;
}
}
pub fn can_perform_pet(position: Pos, target_position: Pos, target_alignment: Alignment) -> bool {
let within_distance = position.0.distance_squared(target_position.0) <= MAX_MOUNT_RANGE.powi(2);
let valid_alignment = matches!(target_alignment, Alignment::Owned(_) | Alignment::Tame);
within_distance && valid_alignment
}
pub fn attempt_pet(data: &JoinData<'_>, update: &mut StateUpdate, target_uid: Uid) {
let can_pet = data
.id_maps
.uid_entity(target_uid)
.and_then(|target_entity| {
data.prev_phys_caches
.get(target_entity)
.and_then(|prev_phys| prev_phys.pos)
.zip(data.alignments.get(target_entity))
})
.map_or(false, |(target_position, target_alignment)| {
can_perform_pet(*data.pos, target_position, *target_alignment)
});
if can_pet && data.physics.on_ground.is_some() && data.body.is_humanoid() {
update.character = CharacterState::Pet(pet::Data {
static_data: pet::StaticData { target_uid },
});
}
}
pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate) {
if data.physics.on_ground.is_some() {
update.character = CharacterState::Talk;
}
}
pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) {
if data.physics.on_ground.is_some() && data.body.is_humanoid() {
update.character = Idle(idle::Data {
is_sneaking: true,
time_entered: *data.time,
footwear: data.character.footwear(),
});
}
}
pub fn handle_climb(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
if data.inputs.climb.is_some()
&& data.physics.on_wall.is_some()
&& data.physics.on_ground.is_none()
&& !data
.physics
.in_liquid()
.map(|depth| depth > 1.0)
.unwrap_or(false)
&& (data.body.can_climb() || data.physics.in_liquid().is_some())
&& update.energy.current() > 1.0
{
update.character = CharacterState::Climb(climb::Data::create_adjusted_by_skills(data));
true
} else {
false
}
}
pub fn handle_wallrun(data: &JoinData<'_>, update: &mut StateUpdate) -> bool {
if data.physics.on_wall.is_some()
&& data.physics.on_ground.is_none()
&& data.physics.in_liquid().is_none()
&& data.body.can_climb()
{
update.character = CharacterState::Wallrun(wallrun::Data {
was_wielded: data.character.is_wield() || data.character.was_wielded(),
});
true
} else {
false
}
}
pub fn attempt_swap_equipped_weapons(
data: &JoinData<'_>,
update: &mut StateUpdate,
output_events: &mut OutputEvents,
) {
if data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::InactiveMainhand))
.is_some()
|| data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::InactiveOffhand))
.is_some()
{
update.swap_equipped_weapons = true;
loadout_change_hook(data, output_events, false);
}
}
fn can_reach_block(
player_pos: Vec3<f32>,
block_pos: Vec3<i32>,
range: f32,
body: &Body,
terrain: &TerrainGrid,
) -> bool {
let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
let block_range_check = |pos: Vec3<f32>| {
(block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
};
let close_to_block = block_range_check(player_pos)
|| block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
if close_to_block {
let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
let heuristic =
move |pos: &Vec3<i32>, _: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
let mut astar = Astar::new(
iters,
player_pos.map(|x| x.floor() as i32),
BuildHasherDefault::<FxHasher64>::default(),
);
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()
};
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| {
let dest = dir + pos;
(dest, transition(pos, dest))
})
.filter(|(pos, _)| {
terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
astar
.poll(iters, heuristic, neighbors, satisfied)
.into_path()
.is_some()
} else {
false
}
}
pub fn handle_manipulate_loadout(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
update: &mut StateUpdate,
inv_action: InventoryAction,
) {
loadout_change_hook(data, output_events, true);
match inv_action {
InventoryAction::Use(slot @ Slot::Inventory(inv_slot)) => {
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();
update.character = CharacterState::UseItem(use_item::Data {
static_data: use_item::StaticData {
buildup_duration,
use_duration,
recover_duration,
inv_slot,
item_kind,
item_hash: item.item_hash(),
was_wielded: data.character.is_wield(),
was_sneak: data.character.is_stealthy(),
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
});
} else {
let inv_manip = InventoryManip::Use(slot);
output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
}
},
InventoryAction::Collect(sprite_pos) => {
let sprite_at_pos = data
.terrain
.get(sprite_pos)
.ok()
.copied()
.and_then(|b| b.get_sprite());
let sprite_interact =
sprite_at_pos.and_then(Option::<sprite_interact::SpriteInteractKind>::from);
if let Some(sprite_interact) = sprite_interact {
if can_reach_block(
data.pos.0,
sprite_pos,
MAX_PICKUP_RANGE,
data.body,
data.terrain,
) {
let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
let sprite_cfg = data
.terrain
.pos_chunk(sprite_pos)
.and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
let required_item =
sprite_at_pos.and_then(|s| match s.unlock_condition(sprite_cfg.cloned()) {
UnlockKind::Free => None,
UnlockKind::Requires(item) => Some((item, false)),
UnlockKind::Consumes(item) => Some((item, true)),
});
let has_required_items = match required_item {
Some((item_id, consume)) => data
.inventory
.and_then(|inv| inv.get_slot_of_item_by_def_id(&item_id))
.map(|slot| Some((item_id, slot, consume))),
None => Some(None),
};
if let Some(required_item) = has_required_items {
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,
was_wielded: data.character.is_wield(),
was_sneak: data.character.is_stealthy(),
required_item,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
})
} else {
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::FailedSpriteUnlock { pos: sprite_pos },
));
}
}
}
},
InventoryAction::Swap(equip, slot) => {
let inv_manip = InventoryManip::Swap(Slot::Equip(equip), slot);
output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
},
InventoryAction::Drop(equip) => {
let inv_manip = InventoryManip::Drop(Slot::Equip(equip));
output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
},
InventoryAction::Sort => {
output_events.emit_server(InventoryManipEvent(data.entity, InventoryManip::Sort));
},
InventoryAction::Use(slot @ Slot::Equip(_)) => {
let inv_manip = InventoryManip::Use(slot);
output_events.emit_server(InventoryManipEvent(data.entity, inv_manip));
},
InventoryAction::Use(Slot::Overflow(_)) => {
},
InventoryAction::ToggleSpriteLight(pos, enable) => {
if matches!(pos.kind, Volume::Terrain) {
let sprite_interact = sprite_interact::SpriteInteractKind::ToggleLight(enable);
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: pos.pos,
sprite_kind: sprite_interact,
was_wielded: data.character.is_wield(),
was_sneak: data.character.is_stealthy(),
required_item: None,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
});
}
},
}
}
pub fn attempt_glide_wield(
data: &JoinData<'_>,
update: &mut StateUpdate,
output_events: &mut OutputEvents,
) {
if data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::Glider))
.is_some()
&& !data
.physics
.in_liquid()
.map(|depth| depth > 1.0)
.unwrap_or(false)
&& data.body.is_humanoid()
{
output_events.emit_local(LocalEvent::CreateOutcome(Outcome::Glider {
pos: data.pos.0,
wielded: true,
}));
update.character = CharacterState::GlideWield(glide_wield::Data::from(data));
}
}
pub fn handle_jump(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
_update: &mut StateUpdate,
strength: f32,
) -> bool {
input_is_pressed(data, InputKind::Jump)
.then(|| data.body.jump_impulse())
.flatten()
.and_then(|impulse| {
if data.physics.in_liquid().is_some() {
if data.physics.on_wall.is_some() {
Some(impulse * 0.75)
} else {
None
}
} else if data.physics.on_ground.is_some() {
Some(impulse)
} else {
None
}
})
.map(|impulse| {
output_events.emit_local(LocalEvent::Jump(
data.entity,
strength * impulse / data.mass.0
* data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25))
* data.stats.jump_modifier,
));
})
.is_some()
}
fn handle_ability(
data: &JoinData<'_>,
update: &mut StateUpdate,
output_events: &mut OutputEvents,
input: InputKind,
) -> bool {
let context = AbilityContext::from(data.stance, data.inventory, data.combo);
if let Some(ability_input) = input.into() {
if let Some((ability, from_offhand, spec_ability)) = data
.active_abilities
.and_then(|a| {
a.activate_ability(
ability_input,
data.inventory,
data.skill_set,
Some(data.body),
Some(data.character),
&context,
Some(data.stats),
)
})
.map(|(mut a, f, s)| {
if let Some(contextual_stats) = a.ability_meta().contextual_stats {
a = a.adjusted_by_stats(contextual_stats.equivalent_stats(data))
}
(a, f, s)
})
.filter(|(ability, _, _)| ability.requirements_paid(data, update))
{
update.character = CharacterState::from((
&ability,
AbilityInfo::new(
data,
from_offhand,
input,
Some(spec_ability),
ability.ability_meta(),
),
data,
));
if let Some(init_event) = ability.ability_meta().init_event {
match init_event {
AbilityInitEvent::EnterStance(stance) => {
output_events.emit_server(ChangeStanceEvent {
entity: data.entity,
stance,
});
},
AbilityInitEvent::GainBuff {
kind,
strength,
duration,
} => {
let dest_info = DestInfo {
stats: Some(data.stats),
mass: Some(data.mass),
};
output_events.emit_server(BuffEvent {
entity: data.entity,
buff_change: BuffChange::Add(Buff::new(
kind,
BuffData::new(strength, duration),
vec![BuffCategory::SelfBuff],
BuffSource::Character { by: *data.uid },
*data.time,
dest_info,
Some(data.mass),
)),
});
},
}
}
if let CharacterState::Roll(roll) = &mut update.character {
if data.character.is_wield() || data.character.was_wielded() {
roll.was_wielded = true;
}
if data.character.is_stealthy() {
roll.is_sneaking = true;
}
if data.character.is_aimed() {
roll.prev_aimed_dir = Some(data.controller.inputs.look_dir);
}
}
return true;
}
}
false
}
pub fn handle_input(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
update: &mut StateUpdate,
input: InputKind,
) {
match input {
InputKind::Primary
| InputKind::Secondary
| InputKind::Ability(_)
| InputKind::Block
| InputKind::Roll => {
handle_ability(data, update, output_events, input);
},
InputKind::Jump => {
handle_jump(data, output_events, update, 1.0);
},
InputKind::Fly => {},
}
}
pub fn handle_glider_input_or(
data: &JoinData<'_>,
update: &mut StateUpdate,
output_events: &mut OutputEvents,
fallback_fn: fn(&JoinData<'_>, &mut StateUpdate),
) {
if data
.inventory
.and_then(|inv| inv.equipped(EquipSlot::Glider))
.and_then(|glider| glider.item_config())
.is_none()
{
fallback_fn(data, update);
return;
};
if let Some(input) = data.controller.queued_inputs.keys().next() {
handle_ability(data, update, output_events, *input);
};
}
pub fn attempt_input(
data: &JoinData<'_>,
output_events: &mut OutputEvents,
update: &mut StateUpdate,
) {
if let Some(input) = data.controller.queued_inputs.keys().next() {
handle_input(data, output_events, update, *input);
}
}
pub fn handle_interrupts(
data: &JoinData,
update: &mut StateUpdate,
output_events: &mut OutputEvents,
) -> bool {
let can_dodge = matches!(
data.character.stage_section(),
Some(StageSection::Buildup | StageSection::Recover)
);
let can_block = data
.character
.ability_info()
.map(|info| info.ability_meta)
.map_or(false, |meta| {
meta.capabilities.contains(Capability::BLOCK_INTERRUPT)
});
if can_dodge && input_is_pressed(data, InputKind::Roll) {
handle_ability(data, update, output_events, InputKind::Roll)
} else if can_block && input_is_pressed(data, InputKind::Block) {
handle_ability(data, update, output_events, InputKind::Block)
} else {
false
}
}
pub fn is_strafing(data: &JoinData<'_>, update: &StateUpdate) -> bool {
(update.character.is_aimed() || update.should_strafe) && data.body.can_strafe()
&& !matches!(unwrap_tool_data(data, EquipSlot::ActiveMainhand),
Some((ToolKind::Instrument, _)))
}
pub fn unwrap_tool_data(data: &JoinData, equip_slot: EquipSlot) -> Option<(ToolKind, Hands)> {
if let Some(ItemKind::Tool(tool)) = data
.inventory
.and_then(|inv| inv.equipped(equip_slot))
.map(|i| i.kind())
.as_deref()
{
Some((tool.kind, tool.hands))
} 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())
.as_deref()
{
Some(tool.hands)
} else {
None
}
};
(
hand(EquipSlot::ActiveMainhand),
hand(EquipSlot::ActiveOffhand),
)
}
pub fn get_tool_stats(data: &JoinData<'_>, ai: AbilityInfo) -> tool::Stats {
ai.hand
.map(|hand| hand.to_equip_slot())
.and_then(|slot| data.inventory.and_then(|inv| inv.equipped(slot)))
.and_then(|item| {
if let ItemKind::Tool(tool) = &*item.kind() {
Some(tool.stats(item.stats_durability_multiplier()))
} else {
None
}
})
.unwrap_or(tool::Stats::one())
}
pub fn input_is_pressed(data: &JoinData<'_>, input: InputKind) -> bool {
data.controller.queued_inputs.contains_key(&input)
}
pub fn checked_tick_attack(
data: &JoinData<'_>,
timer: Duration,
other_modifier: Option<f32>,
) -> Option<Duration> {
timer.checked_add(Duration::from_secs_f32(
data.dt.0 * data.stats.attack_speed_modifier * other_modifier.unwrap_or(1.0),
))
}
pub fn tick_attack_or_default(
data: &JoinData<'_>,
timer: Duration,
other_modifier: Option<f32>,
) -> Duration {
checked_tick_attack(data, timer, other_modifier).unwrap_or_default()
}
#[derive(Clone, Copy, Debug, Display, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub enum StageSection {
Buildup,
Recover,
Charge,
Movement,
Action,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum ForcedMovement {
Forward(f32),
Reverse(f32),
Sideways(f32),
DirectedReverse(f32),
AntiDirectedForward(f32),
Leap {
vertical: f32,
forward: f32,
progress: f32,
direction: MovementDirection,
},
Hover {
move_input: f32,
},
}
impl Mul<f32> for ForcedMovement {
type Output = Self;
fn mul(self, scalar: f32) -> Self {
use ForcedMovement::*;
match self {
Forward(x) => Forward(x * scalar),
Reverse(x) => Reverse(x * scalar),
Sideways(x) => Sideways(x * scalar),
DirectedReverse(x) => DirectedReverse(x * scalar),
AntiDirectedForward(x) => AntiDirectedForward(x * scalar),
Leap {
vertical,
forward,
progress,
direction,
} => Leap {
vertical: vertical * scalar,
forward: forward * scalar,
progress,
direction,
},
Hover { move_input } => Hover { move_input },
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, 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>,
pub input: InputKind,
pub input_attr: Option<InputAttr>,
pub ability_meta: AbilityMeta,
pub ability: Option<SpecifiedAbility>,
}
impl AbilityInfo {
pub fn new(
data: &JoinData<'_>,
from_offhand: bool,
input: InputKind,
ability: Option<SpecifiedAbility>,
ability_meta: AbilityMeta,
) -> Self {
let tool_data = if from_offhand {
unwrap_tool_data(data, EquipSlot::ActiveOffhand)
} else {
unwrap_tool_data(data, EquipSlot::ActiveMainhand)
};
let (tool, hand) = tool_data.map_or((None, None), |(kind, hands)| {
(
Some(kind),
Some(HandInfo::from_main_tool(hands, from_offhand)),
)
});
Self {
tool,
hand,
input,
input_attr: data.controller.queued_inputs.get(&input).copied(),
ability_meta,
ability,
}
}
}
pub fn end_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
if data.character.is_wield() || data.character.was_wielded() {
update.character = CharacterState::Wielding(wielding::Data {
is_sneaking: data.character.is_stealthy(),
});
} else {
update.character = CharacterState::Idle(idle::Data {
is_sneaking: data.character.is_stealthy(),
footwear: None,
time_entered: *data.time,
});
}
if let CharacterState::Roll(roll) = data.character {
if let Some(dir) = roll.prev_aimed_dir {
update.ori = dir.into();
}
}
}
pub fn end_melee_ability(data: &JoinData<'_>, update: &mut StateUpdate) {
end_ability(data, update);
data.updater.remove::<Melee>(data.entity);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum HandInfo {
TwoHanded,
MainHand,
OffHand,
}
impl HandInfo {
pub fn from_main_tool(tool_hands: Hands, from_offhand: bool) -> Self {
match tool_hands {
Hands::Two => Self::TwoHanded,
Hands::One => {
if from_offhand {
Self::OffHand
} else {
Self::MainHand
}
},
}
}
pub fn to_equip_slot(&self) -> EquipSlot {
match self {
HandInfo::TwoHanded | HandInfo::MainHand => EquipSlot::ActiveMainhand,
HandInfo::OffHand => EquipSlot::ActiveOffhand,
}
}
}
pub fn leave_stance(data: &JoinData<'_>, output_events: &mut OutputEvents) {
if !matches!(data.stance, Some(Stance::None)) {
output_events.emit_server(ChangeStanceEvent {
entity: data.entity,
stance: Stance::None,
});
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScalingKind {
Linear,
Sqrt,
}
impl ScalingKind {
pub fn factor(&self, val: f32, norm: f32) -> f32 {
match self {
Self::Linear => val / norm,
Self::Sqrt => (val / norm).sqrt(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum ComboConsumption {
#[default]
All,
Half,
Cost,
}
impl ComboConsumption {
pub fn consume(&self, data: &JoinData, output_events: &mut OutputEvents, cost: u32) {
let combo = data.combo.map_or(0, |c| c.counter());
let to_consume = match self {
Self::All => combo,
Self::Half => (combo + 1) / 2,
Self::Cost => cost,
};
output_events.emit_server(ComboChangeEvent {
entity: data.entity,
change: -(to_consume as i32),
});
}
}
fn loadout_change_hook(data: &JoinData<'_>, output_events: &mut OutputEvents, clear_combo: bool) {
if clear_combo {
output_events.emit_server(ComboChangeEvent {
entity: data.entity,
change: -data.combo.map_or(0, |c| c.counter() as i32),
});
}
output_events.emit_server(BuffEvent {
entity: data.entity,
buff_change: BuffChange::RemoveByCategory {
all_required: vec![BuffCategory::RemoveOnLoadoutChange],
any_required: vec![],
none_required: vec![],
},
});
}