mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'swilliams/hitbox-adjustments' into 'master'
General NPC Hitbox Adjustments See merge request veloren/veloren!1723
This commit is contained in:
commit
bdbed58b09
@ -37,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Changed camera smoothing to be off by default.
|
||||
- Fixed AI behavior so only humanoids will attempt to roll
|
||||
- Footstep SFX is now dependant on distance moved, not time since last play
|
||||
- Increased the hitbox of the Stonework Defender and Mindflayer to better fit their models.
|
||||
- Adjusted most NPCs hitboxes to better fit their models.
|
||||
|
||||
### Removed
|
||||
|
||||
|
@ -141,49 +141,62 @@ impl Body {
|
||||
// TODO: Improve these values (some might be reliant on more info in inner type)
|
||||
match self {
|
||||
Body::Humanoid(humanoid) => match (humanoid.species, humanoid.body_type) {
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Male) => 0.57,
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Female) => 0.51,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Male) => 0.51,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Female) => 0.48,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Male) => 0.51,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Female) => 0.48,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Male) => 0.42,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Female) => 0.39,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Male) => 0.48,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Female) => 0.45,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Male) => 0.348,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Female) => 0.348,
|
||||
_ => 0.5,
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Male) => 0.75,
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Female) => 0.75,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Male) => 0.75,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Female) => 0.75,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Male) => 0.75,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Female) => 0.75,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Male) => 0.75,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Female) => 0.75,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Male) => 0.75,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Female) => 0.75,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Male) => 0.75,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Female) => 0.75,
|
||||
_ => 0.75,
|
||||
},
|
||||
Body::QuadrupedSmall(_) => 0.4,
|
||||
Body::QuadrupedSmall(_) => 0.7,
|
||||
Body::QuadrupedMedium(body) => match body.species {
|
||||
quadruped_medium::Species::Grolgar => 1.9,
|
||||
quadruped_medium::Species::Tarasque => 1.8,
|
||||
quadruped_medium::Species::Lion => 1.9,
|
||||
quadruped_medium::Species::Saber => 1.8,
|
||||
quadruped_medium::Species::Catoblepas => 1.7,
|
||||
quadruped_medium::Species::Grolgar => 2.0,
|
||||
quadruped_medium::Species::Tarasque => 2.0,
|
||||
quadruped_medium::Species::Lion => 2.0,
|
||||
quadruped_medium::Species::Saber => 2.0,
|
||||
quadruped_medium::Species::Catoblepas => 2.0,
|
||||
quadruped_medium::Species::Horse => 1.5,
|
||||
quadruped_medium::Species::Deer => 1.5,
|
||||
quadruped_medium::Species::Donkey => 1.5,
|
||||
quadruped_medium::Species::Kelpie => 1.5,
|
||||
_ => 1.5,
|
||||
},
|
||||
Body::QuadrupedLow(body) => match body.species {
|
||||
quadruped_low::Species::Asp => 1.8,
|
||||
quadruped_low::Species::Monitor => 1.75,
|
||||
quadruped_low::Species::Crocodile => 2.1,
|
||||
quadruped_low::Species::Salamander => 1.9,
|
||||
quadruped_low::Species::Pangolin => 1.3,
|
||||
quadruped_low::Species::Asp => 2.5,
|
||||
quadruped_low::Species::Monitor => 2.3,
|
||||
quadruped_low::Species::Crocodile => 2.4,
|
||||
quadruped_low::Species::Salamander => 2.4,
|
||||
quadruped_low::Species::Pangolin => 2.0,
|
||||
quadruped_low::Species::Lavadrake => 2.5,
|
||||
_ => 1.6,
|
||||
},
|
||||
Body::Theropod(body) => match body.species {
|
||||
theropod::Species::Snowraptor => 0.5,
|
||||
theropod::Species::Sandraptor => 0.5,
|
||||
theropod::Species::Woodraptor => 0.5,
|
||||
theropod::Species::Snowraptor => 1.5,
|
||||
theropod::Species::Sandraptor => 1.5,
|
||||
theropod::Species::Woodraptor => 1.5,
|
||||
theropod::Species::Archaeos => 4.5,
|
||||
theropod::Species::Odonto => 4.5,
|
||||
_ => 1.8,
|
||||
},
|
||||
Body::BirdMedium(_) => 0.35,
|
||||
Body::FishMedium(_) => 0.35,
|
||||
Body::BirdMedium(_) => 1.0,
|
||||
Body::FishMedium(_) => 1.0,
|
||||
Body::Dragon(_) => 8.0,
|
||||
Body::BirdSmall(_) => 0.3,
|
||||
Body::FishSmall(_) => 0.3,
|
||||
Body::BipedLarge(_) => 1.5,
|
||||
Body::BirdSmall(_) => 0.6,
|
||||
Body::FishSmall(_) => 0.6,
|
||||
Body::BipedLarge(body) => match body.species {
|
||||
biped_large::Species::Slysaurok => 2.3,
|
||||
biped_large::Species::Occultsaurok => 2.8,
|
||||
biped_large::Species::Mightysaurok => 2.3,
|
||||
biped_large::Species::Mindflayer => 1.8,
|
||||
_ => 4.6,
|
||||
},
|
||||
Body::Golem(_) => 2.5,
|
||||
Body::Object(_) => 0.4,
|
||||
}
|
||||
@ -192,18 +205,18 @@ impl Body {
|
||||
pub fn height(&self) -> f32 {
|
||||
match self {
|
||||
Body::Humanoid(humanoid) => match (humanoid.species, humanoid.body_type) {
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Male) => 2.17,
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Female) => 1.94,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Male) => 1.94,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Female) => 1.82,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Male) => 1.94,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Female) => 1.82,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Male) => 1.60,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Female) => 1.48,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Male) => 1.82,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Female) => 1.71,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Male) => 1.32,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Female) => 1.32,
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Male) => 2.3,
|
||||
(humanoid::Species::Orc, humanoid::BodyType::Female) => 2.2,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Male) => 2.3,
|
||||
(humanoid::Species::Human, humanoid::BodyType::Female) => 2.2,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Male) => 2.3,
|
||||
(humanoid::Species::Elf, humanoid::BodyType::Female) => 2.2,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Male) => 1.9,
|
||||
(humanoid::Species::Dwarf, humanoid::BodyType::Female) => 1.8,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Male) => 2.2,
|
||||
(humanoid::Species::Undead, humanoid::BodyType::Female) => 2.1,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Male) => 1.5,
|
||||
(humanoid::Species::Danari, humanoid::BodyType::Female) => 1.4,
|
||||
},
|
||||
Body::QuadrupedSmall(body) => match body.species {
|
||||
quadruped_small::Species::Dodarock => 1.5,
|
||||
@ -212,10 +225,10 @@ impl Body {
|
||||
_ => 1.0,
|
||||
},
|
||||
Body::QuadrupedMedium(body) => match body.species {
|
||||
quadruped_medium::Species::Tarasque => 2.5,
|
||||
quadruped_medium::Species::Lion => 1.8,
|
||||
quadruped_medium::Species::Saber => 1.8,
|
||||
quadruped_medium::Species::Catoblepas => 2.8,
|
||||
quadruped_medium::Species::Tarasque => 2.6,
|
||||
quadruped_medium::Species::Lion => 2.0,
|
||||
quadruped_medium::Species::Saber => 2.0,
|
||||
quadruped_medium::Species::Catoblepas => 2.9,
|
||||
_ => 1.6,
|
||||
},
|
||||
Body::QuadrupedLow(body) => match body.species {
|
||||
@ -226,9 +239,9 @@ impl Body {
|
||||
_ => 1.3,
|
||||
},
|
||||
Body::Theropod(body) => match body.species {
|
||||
theropod::Species::Snowraptor => 2.5,
|
||||
theropod::Species::Sandraptor => 2.5,
|
||||
theropod::Species::Woodraptor => 2.5,
|
||||
theropod::Species::Snowraptor => 2.6,
|
||||
theropod::Species::Sandraptor => 2.6,
|
||||
theropod::Species::Woodraptor => 2.6,
|
||||
_ => 8.0,
|
||||
},
|
||||
Body::BirdMedium(body) => match body.species {
|
||||
|
@ -199,12 +199,13 @@ impl<'a> System<'a> for Sys {
|
||||
const LISTEN_DIST: f32 = 16.0;
|
||||
const SEARCH_DIST: f32 = 48.0;
|
||||
const SIGHT_DIST: f32 = 80.0;
|
||||
const MIN_ATTACK_DIST: f32 = 2.0;
|
||||
const MAX_FLEE_DIST: f32 = 20.0;
|
||||
const SNEAK_COEFFICIENT: f32 = 0.25;
|
||||
|
||||
let scale = scales.get(entity).map(|s| s.0).unwrap_or(1.0);
|
||||
|
||||
let min_attack_dist = body.map_or(2.0, |b| b.radius() * scale * 1.5);
|
||||
|
||||
// This controls how picky NPCs are about their pathfinding. Giants are larger
|
||||
// and so can afford to be less precise when trying to move around
|
||||
// the world (especially since they would otherwise get stuck on
|
||||
@ -537,7 +538,7 @@ impl<'a> System<'a> for Sys {
|
||||
// depending on the distance from the agent to the target
|
||||
match tactic {
|
||||
Tactic::Melee => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.primary.set_state(true);
|
||||
inputs.move_dir = Vec2::zero();
|
||||
} else if dist_sqrd < MAX_CHASE_DIST.powi(2)
|
||||
@ -575,7 +576,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Axe => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
if *powerup > 6.0 {
|
||||
inputs.secondary.set_state(false);
|
||||
@ -624,7 +625,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Hammer => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
if *powerup > 4.0 {
|
||||
inputs.secondary.set_state(false);
|
||||
@ -688,7 +689,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Sword => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
if stats.skill_set.has_skill(Skill::Sword(SwordSkill::UnlockSpin)) && *powerup < 2.0 && energy.current() > 600 {
|
||||
inputs.ability3.set_state(true);
|
||||
@ -747,7 +748,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Bow => {
|
||||
if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (2.0 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) {
|
||||
inputs.roll.set_state(true);
|
||||
} else if dist_sqrd < MAX_CHASE_DIST.powi(2)
|
||||
|| (dist_sqrd < SIGHT_DIST.powi(2) && !*been_close)
|
||||
@ -813,10 +814,10 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Staff => {
|
||||
if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if body.map(|b| b.is_humanoid()).unwrap_or(false) && dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.roll.set_state(true);
|
||||
} else if dist_sqrd
|
||||
< (5.0 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
< (5.0 * min_attack_dist * scale).powi(2)
|
||||
{
|
||||
if *powerup < 1.5 {
|
||||
inputs.move_dir = (tgt_pos.0 - pos.0)
|
||||
@ -890,7 +891,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::StoneGolemBoss => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale * 2.0).powi(2) { // 2.0 is temporary correction factor to allow them to melee with their large hitbox
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
inputs.primary.set_state(true);
|
||||
} else if dist_sqrd < MAX_CHASE_DIST.powi(2)
|
||||
@ -942,23 +943,23 @@ impl<'a> System<'a> for Sys {
|
||||
radius,
|
||||
circle_time,
|
||||
} => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2)
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2)
|
||||
&& thread_rng().gen_bool(0.5)
|
||||
{
|
||||
inputs.move_dir = Vec2::zero();
|
||||
inputs.primary.set_state(true);
|
||||
} else if dist_sqrd
|
||||
< (radius as f32 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
< (radius as f32 * min_attack_dist * scale).powi(2)
|
||||
{
|
||||
inputs.move_dir = (pos.0 - tgt_pos.0)
|
||||
.xy()
|
||||
.try_normalized()
|
||||
.unwrap_or(Vec2::unit_y());
|
||||
} else if dist_sqrd
|
||||
< ((radius as f32 + 1.0) * MIN_ATTACK_DIST * scale)
|
||||
< ((radius as f32 + 1.0) * min_attack_dist * scale)
|
||||
.powi(2)
|
||||
&& dist_sqrd
|
||||
> (radius as f32 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
> (radius as f32 * min_attack_dist * scale).powi(2)
|
||||
{
|
||||
if *powerup < circle_time as f32 {
|
||||
inputs.move_dir = (tgt_pos.0 - pos.0)
|
||||
@ -1012,7 +1013,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::QuadLowRanged => {
|
||||
if dist_sqrd < (5.0 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (5.0 * min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = (tgt_pos.0 - pos.0)
|
||||
.xy()
|
||||
.try_normalized()
|
||||
@ -1074,7 +1075,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::TailSlap => {
|
||||
if dist_sqrd < (1.5 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) {
|
||||
if *powerup > 4.0 {
|
||||
inputs.primary.set_state(false);
|
||||
*powerup = 0.0;
|
||||
@ -1119,12 +1120,12 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::QuadLowQuick => {
|
||||
if dist_sqrd < (1.5 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
inputs.secondary.set_state(true);
|
||||
} else if dist_sqrd
|
||||
< (3.0 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
&& dist_sqrd > (2.0 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
< (3.0 * min_attack_dist * scale).powi(2)
|
||||
&& dist_sqrd > (2.0 * min_attack_dist * scale).powi(2)
|
||||
{
|
||||
inputs.primary.set_state(true);
|
||||
inputs.move_dir = (tgt_pos.0 - pos.0)
|
||||
@ -1161,7 +1162,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::QuadLowBasic => {
|
||||
if dist_sqrd < (1.5 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
if *powerup > 5.0 {
|
||||
*powerup = 0.0;
|
||||
@ -1201,11 +1202,11 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::QuadMedJump => {
|
||||
if dist_sqrd < (1.5 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (1.5 * min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
inputs.secondary.set_state(true);
|
||||
} else if dist_sqrd
|
||||
< (5.0 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
< (5.0 * min_attack_dist * scale).powi(2)
|
||||
{
|
||||
inputs.ability3.set_state(true);
|
||||
} else if dist_sqrd < MAX_CHASE_DIST.powi(2)
|
||||
@ -1246,7 +1247,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::QuadMedBasic => {
|
||||
if dist_sqrd < (MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
if *powerup < 2.0 {
|
||||
inputs.secondary.set_state(true);
|
||||
@ -1286,11 +1287,11 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Lavadrake => {
|
||||
if dist_sqrd < (2.5 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (2.5 * min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
inputs.secondary.set_state(true);
|
||||
} else if dist_sqrd
|
||||
< (7.0 * MIN_ATTACK_DIST * scale).powi(2)
|
||||
< (7.0 * min_attack_dist * scale).powi(2)
|
||||
{
|
||||
if *powerup < 2.0 {
|
||||
inputs.move_dir = (tgt_pos.0 - pos.0)
|
||||
@ -1343,7 +1344,7 @@ impl<'a> System<'a> for Sys {
|
||||
}
|
||||
},
|
||||
Tactic::Theropod => {
|
||||
if dist_sqrd < (2.0 * MIN_ATTACK_DIST * scale).powi(2) {
|
||||
if dist_sqrd < (2.0 * min_attack_dist * scale).powi(2) {
|
||||
inputs.move_dir = Vec2::zero();
|
||||
inputs.primary.set_state(true);
|
||||
} else if dist_sqrd < MAX_CHASE_DIST.powi(2)
|
||||
|
@ -58,13 +58,14 @@ impl<'a> System<'a> for Sys {
|
||||
let mut server_emitter = server_bus.emitter();
|
||||
let _local_emitter = local_bus.emitter();
|
||||
// Attacks
|
||||
for (entity, uid, pos, ori, scale_maybe, attack) in (
|
||||
for (entity, uid, pos, ori, scale_maybe, attack, body) in (
|
||||
&entities,
|
||||
&uids,
|
||||
&positions,
|
||||
&orientations,
|
||||
scales.maybe(),
|
||||
&mut attacking_storage,
|
||||
&bodies,
|
||||
)
|
||||
.join()
|
||||
{
|
||||
@ -92,6 +93,7 @@ impl<'a> System<'a> for Sys {
|
||||
// Scales
|
||||
let scale = scale_maybe.map_or(1.0, |s| s.0);
|
||||
let scale_b = scale_b_maybe.map_or(1.0, |s| s.0);
|
||||
let rad = body.radius() * scale;
|
||||
let rad_b = body_b.radius() * scale_b;
|
||||
|
||||
// Check if entity is dodging
|
||||
@ -101,7 +103,7 @@ impl<'a> System<'a> for Sys {
|
||||
if entity != b
|
||||
&& !health_b.is_dead
|
||||
// Spherical wedge shaped attack field
|
||||
&& pos.0.distance_squared(pos_b.0) < (rad_b + scale * attack.range).powi(2)
|
||||
&& pos.0.distance_squared(pos_b.0) < (rad + rad_b + scale * attack.range).powi(2)
|
||||
&& ori2.angle_between(pos_b2 - pos2) < attack.max_angle + (rad_b / pos2.distance(pos_b2)).atan()
|
||||
{
|
||||
// See if entities are in the same group
|
||||
|
Loading…
Reference in New Issue
Block a user