Compare commits
2 Commits
920c5c27fb
...
7c81e03613
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c81e03613 | |||
| 022f0948db |
@@ -7,40 +7,79 @@ public partial class Actor : Node
|
|||||||
{
|
{
|
||||||
public bool _available, _hovered, _selected;
|
public bool _available, _hovered, _selected;
|
||||||
public List<Cue> _cues = new();
|
public List<Cue> _cues = new();
|
||||||
public Cue _activeCue;
|
public Cue _selectedCue = null;
|
||||||
public List<Ball> _balls = new();
|
public List<Ball> _balls = new();
|
||||||
public Ball _activeBall;
|
public List<Ball> _ballBag = new();
|
||||||
|
public Ball _hoveredBall = null;
|
||||||
|
public Ball _selectedBall = null;
|
||||||
|
public Ball _heldBall = null;
|
||||||
|
|
||||||
|
public Sprite2D _tempBallSprite = new();
|
||||||
|
|
||||||
public static Actor Create(string SCENENAME)
|
public static Actor Create(string SCENENAME)
|
||||||
{
|
{
|
||||||
PackedScene scene = ResourceLoader.Load<PackedScene>("res://Gameplay/"+SCENENAME+".tscn");
|
PackedScene scene = ResourceLoader.Load<PackedScene>("res://Gameplay/" + SCENENAME + ".tscn");
|
||||||
Actor newActor = scene.Instantiate<Actor>();
|
Actor newActor = scene.Instantiate<Actor>();
|
||||||
return newActor;
|
return newActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
Ball newBall = Ball.Create("ball", 0);
|
Ball newBall = Ball.Create("ball", 0);
|
||||||
AddChild(newBall);
|
|
||||||
_balls.Add(newBall);
|
_balls.Add(newBall);
|
||||||
if (_activeBall == null)
|
newBall = Ball.Create("ball", 1);
|
||||||
{
|
_balls.Add(newBall);
|
||||||
_activeBall = _balls[0];
|
|
||||||
}
|
|
||||||
Cue newCue = Cue.Create("cue");
|
Cue newCue = Cue.Create("cue");
|
||||||
AddChild(newCue);
|
AddChild(newCue);
|
||||||
_cues.Add(newCue);
|
_cues.Add(newCue);
|
||||||
if (_activeCue == null)
|
for (int i = 0; i < _cues.Count; i++)
|
||||||
{
|
{
|
||||||
_activeCue = _cues[0];
|
_cues[i].Shoot += OnCueShoot;
|
||||||
_activeCue.Shoot += OnCueShoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ballBag = new(_balls);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Process(double DELTA_)
|
public override void _Process(double DELTA_)
|
||||||
{
|
{
|
||||||
if (!_activeBall._placed)
|
if (_ballBag.Count > 0)
|
||||||
{
|
{
|
||||||
|
<<<<<<< HEAD
|
||||||
|
if (_heldBall == null)
|
||||||
|
{
|
||||||
|
_heldBall = _ballBag[0];
|
||||||
|
}
|
||||||
|
else if (Input.IsActionJustPressed("scroll_up"))
|
||||||
|
{
|
||||||
|
_heldBall = _ballBag[(_ballBag.IndexOf(_heldBall) + 1) % _ballBag.Count];
|
||||||
|
}
|
||||||
|
else if (Input.IsActionJustPressed("scroll_down"))
|
||||||
|
{
|
||||||
|
_heldBall = _ballBag[(_ballBag.IndexOf(_heldBall) - 1) < 0 ? ^1 : (_ballBag.IndexOf(_heldBall) - 1)];
|
||||||
|
}
|
||||||
|
Vector2 mousePosition = GetViewport().GetMousePosition();
|
||||||
|
if (_tempBallSprite.Texture == null)
|
||||||
|
{
|
||||||
|
if (GetChildren().All(n => n != _tempBallSprite))
|
||||||
|
{
|
||||||
|
AddChild(_tempBallSprite);
|
||||||
|
}
|
||||||
|
_tempBallSprite.Texture = _heldBall.GetNode<Sprite2D>("Image").Texture;
|
||||||
|
}
|
||||||
|
_tempBallSprite.Position = mousePosition;
|
||||||
|
if (Input.IsActionJustReleased("left_click"))
|
||||||
|
{
|
||||||
|
_heldBall.Place(mousePosition);
|
||||||
|
AddChild(_heldBall);
|
||||||
|
int ballIndex = _ballBag.IndexOf(_heldBall);
|
||||||
|
_ballBag.Remove(_heldBall);
|
||||||
|
_tempBallSprite.Texture = null;
|
||||||
|
if (_ballBag.Count > 0)
|
||||||
|
{
|
||||||
|
_heldBall = _ballBag[ballIndex - (ballIndex > _ballBag.Count ? 1 : 0)];
|
||||||
|
}
|
||||||
|
=======
|
||||||
_activeCue.HideCue();
|
_activeCue.HideCue();
|
||||||
Vector2 mousePosition = GetViewport().GetMousePosition();
|
Vector2 mousePosition = GetViewport().GetMousePosition();
|
||||||
GetNode<Marker2D>("StartPosition").Position = mousePosition;
|
GetNode<Marker2D>("StartPosition").Position = mousePosition;
|
||||||
@@ -48,44 +87,79 @@ public partial class Actor : Node
|
|||||||
{
|
{
|
||||||
_activeBall.Place(GetNode<Marker2D>("StartPosition").Position);
|
_activeBall.Place(GetNode<Marker2D>("StartPosition").Position);
|
||||||
GD.Print(_activeBall.Position);
|
GD.Print(_activeBall.Position);
|
||||||
|
>>>>>>> 920c5c27fb732c902987b01dcc093e55b1552a9f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_hovered = _activeBall._hovered;
|
if (_balls.Any(b => b._hovered))
|
||||||
if (_activeCue._shown)
|
|
||||||
{
|
{
|
||||||
if (!_activeBall._available)
|
_hoveredBall = _balls.Single(b => b._hovered);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_hoveredBall = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectedCue == null && _cues.Count > 0)
|
||||||
|
{
|
||||||
|
_selectedCue = _cues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((_selectedBall == null || _selectedBall != _hoveredBall) && (_hoveredBall?._available ?? false))
|
||||||
|
{
|
||||||
|
if (Input.IsActionJustReleased("left_click"))
|
||||||
{
|
{
|
||||||
_activeCue.HideCue();
|
GD.Print(_selectedBall);
|
||||||
|
_selectedBall = _hoveredBall;
|
||||||
|
_selectedBall._selected = true;
|
||||||
|
_selectedCue.Don(_selectedBall);
|
||||||
|
GD.Print(_selectedBall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else // (!_activeCue._shown)
|
else if (Input.IsActionJustReleased("right_click"))
|
||||||
{
|
{
|
||||||
if (_activeBall._available)
|
GD.Print(2);
|
||||||
|
_selectedBall._selected = false;
|
||||||
|
_selectedBall = null;
|
||||||
|
_selectedCue.Doff();
|
||||||
|
}
|
||||||
|
else if (_hoveredBall == null)
|
||||||
|
{
|
||||||
|
if (Input.IsActionJustReleased("left_click") && _selectedCue._power == 0)
|
||||||
{
|
{
|
||||||
_activeCue.ShowCue(_activeBall);
|
GD.Print(3);
|
||||||
|
_selectedBall._selected = false;
|
||||||
|
_selectedBall = null;
|
||||||
|
_selectedCue.Doff();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//public void ResetCueBall()
|
//public void ResetCueBall()
|
||||||
//{
|
//{
|
||||||
//_cueBall = Ball.Create("ball", 0, _startPosition);
|
//_cueBall = Ball.Create("ball", 0, _startPosition);
|
||||||
//_cueBall.SetName("CueBall");
|
//_cueBall.SetName("CueBall");
|
||||||
//AddChild(_cueBall);
|
//AddChild(_cueBall);
|
||||||
//Texture2D image = GD.Load<Texture2D>("res://art/cue_ball.png");
|
//Texture2D image = GD.Load<Texture2D>("res://art/cue_ball.png");
|
||||||
//_cueBall.GetNode<Sprite2D>("Image").Texture = image;
|
//_cueBall.GetNode<Sprite2D>("Image").Texture = image;
|
||||||
//_cueBall._placed = true;
|
//_cueBall._placed = true;
|
||||||
//_balls = GetTree().GetNodesInGroup("balls").Select(b => (Ball)b).ToList<Ball>();
|
//_balls = GetTree().GetNodesInGroup("balls").Select(b => (Ball)b).ToList<Ball>();
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void OnCueShoot(Vector2 IMPULSE)
|
private void OnCueShoot(Vector2 IMPULSE)
|
||||||
{
|
{
|
||||||
_activeBall.ApplyCentralImpulse(IMPULSE);
|
if (_selectedBall != null && _selectedBall._placed)
|
||||||
|
{
|
||||||
|
_selectedBall.ApplyCentralImpulse(IMPULSE);
|
||||||
|
_selectedBall._selected = false;
|
||||||
|
_selectedBall = null;
|
||||||
|
_selectedCue.Doff();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
public partial class Ball : RigidBody2D
|
public partial class Ball : RigidBody2D
|
||||||
{
|
{
|
||||||
@@ -26,9 +27,7 @@ public partial class Ball : RigidBody2D
|
|||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
SetProcess(false);
|
_available = true;
|
||||||
_placed = false;
|
|
||||||
_available = false;
|
|
||||||
_hovered = false;
|
_hovered = false;
|
||||||
_selected = false;
|
_selected = false;
|
||||||
_aimed = false;
|
_aimed = false;
|
||||||
@@ -54,28 +53,26 @@ public partial class Ball : RigidBody2D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Globals.Instance._anyMovement)
|
if (Globals.Instance._anyMovement)
|
||||||
{
|
|
||||||
if (!_available)
|
|
||||||
{
|
|
||||||
_available = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
if (_available)
|
if (_available)
|
||||||
{
|
{
|
||||||
_available = false;
|
_available = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!_available)
|
||||||
|
{
|
||||||
|
_available = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Place(Vector2 POSITION)
|
public void Place(Vector2 POSITION)
|
||||||
{
|
{
|
||||||
_placed = true;
|
_placed = true;
|
||||||
Position = POSITION;
|
Position = POSITION;
|
||||||
SetProcess(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseEntered()
|
private void OnMouseEntered()
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ using System.Linq;
|
|||||||
|
|
||||||
public partial class Battle : Node
|
public partial class Battle : Node
|
||||||
{
|
{
|
||||||
[Signal]
|
|
||||||
public delegate void SetCurrentEventHandler(Battle BATTLE);
|
|
||||||
//[Signal]
|
|
||||||
//public delegate void DetectMovementEventHandler(bool BOOL);
|
|
||||||
|
|
||||||
public bool _current;
|
public bool _current;
|
||||||
public Vector2 _startPosition = new Vector2(890, 340);
|
public Vector2 _startPosition = new Vector2(890, 340);
|
||||||
public Player _player;
|
public Player _player;
|
||||||
@@ -83,13 +79,19 @@ public partial class Battle : Node
|
|||||||
|
|
||||||
public void PottedBall(Node2D BODY)
|
public void PottedBall(Node2D BODY)
|
||||||
{
|
{
|
||||||
if (BODY.GetType() != typeof(Ball)){
|
if (BODY.GetType() != typeof(Ball))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (BODY == _player._actor._activeBall)
|
if (BODY == _player._actor._selectedBall)
|
||||||
{
|
{
|
||||||
|
<<<<<<< HEAD
|
||||||
|
_player._actor._selectedBall._potted = true;
|
||||||
|
_player._actor._selectedBall._placed = false;
|
||||||
|
=======
|
||||||
_player._actor._activeBall._potted = true;
|
_player._actor._activeBall._potted = true;
|
||||||
_player._actor._activeBall._placed = false;
|
_player._actor._activeBall._placed = false;
|
||||||
|
>>>>>>> 920c5c27fb732c902987b01dcc093e55b1552a9f
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -108,8 +110,7 @@ public partial class Battle : Node
|
|||||||
{
|
{
|
||||||
_current = true;
|
_current = true;
|
||||||
GenerateBalls();
|
GenerateBalls();
|
||||||
|
Globals.Instance._currentBattle = this;
|
||||||
EmitSignal(SignalName.SetCurrent, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ public partial class Cue : Sprite2D
|
|||||||
[Signal]
|
[Signal]
|
||||||
public delegate void ShootEventHandler(Vector2 IMPULSE);
|
public delegate void ShootEventHandler(Vector2 IMPULSE);
|
||||||
|
|
||||||
public bool _shown;
|
public bool _donned = false, _equiped = false, _sending = false;
|
||||||
public int _powerDirection = 1;
|
public float _power = 0.0f, _maxPower = 20.0f;
|
||||||
public float _power = 0.0f, _maxPower = 8.0f;
|
Vector2 _direction;
|
||||||
public ProgressBar _progressBar;
|
public ProgressBar _progressBar;
|
||||||
|
|
||||||
public static Cue Create(string SCENENAME)
|
public static Cue Create(string SCENENAME)
|
||||||
@@ -24,51 +24,66 @@ public partial class Cue : Sprite2D
|
|||||||
{
|
{
|
||||||
//_progressBar = GetParent().GetNode<ProgressBar>("PowerBar");
|
//_progressBar = GetParent().GetNode<ProgressBar>("PowerBar");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Process(double DELTA_)
|
public override void _Process(double DELTA_)
|
||||||
{
|
{
|
||||||
Vector2 mousePosition = GetViewport().GetMousePosition();
|
|
||||||
LookAt(mousePosition);
|
if (!_sending)
|
||||||
if (Input.IsActionPressed("left_click"))
|
|
||||||
{
|
{
|
||||||
_power += 0.1f * _powerDirection;
|
Vector2 mousePosition = GetViewport().GetMousePosition();
|
||||||
if (_power >= _maxPower)
|
Offset = new Vector2(_power * -10, 0);
|
||||||
|
LookAt(mousePosition);
|
||||||
|
if (Input.IsActionJustPressed("scroll_down"))
|
||||||
{
|
{
|
||||||
_powerDirection = -1;
|
_power = Math.Min(_power + (1f * (Input.IsActionPressed("shift") ? 5 : 1) / (Input.IsActionPressed("ctrl") ? 10 : 1)), _maxPower);
|
||||||
}
|
}
|
||||||
else if (_power <= 0)
|
else if (Input.IsActionJustPressed("scroll_up"))
|
||||||
{
|
{
|
||||||
_powerDirection = 1;
|
_power = Math.Max(_power - (1f * (Input.IsActionPressed("shift") ? 5 : 1) / (Input.IsActionPressed("ctrl") ? 10 : 1)), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Input.IsActionJustReleased("left_click"))
|
||||||
|
{
|
||||||
|
if (_power > 0f)
|
||||||
|
{
|
||||||
|
_sending = true;
|
||||||
|
_direction = mousePosition - Position;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_power > 0f)
|
if (Offset.X < 0)
|
||||||
{
|
{
|
||||||
_powerDirection = 1;
|
Offset = new Vector2(Math.Min(0, Offset.X + _power * 2), 0);
|
||||||
Vector2 direction = mousePosition - Position;
|
}
|
||||||
EmitSignal(SignalName.Shoot, _power * direction);
|
else
|
||||||
|
{
|
||||||
|
_sending = false;
|
||||||
|
Offset = Vector2.Zero;
|
||||||
|
EmitSignal(SignalName.Shoot, _power * _direction);
|
||||||
_power = 0;
|
_power = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HideCue()
|
public void Doff()
|
||||||
{
|
{
|
||||||
_shown = false;
|
_donned = false;
|
||||||
SetProcess(false);
|
SetProcess(false);
|
||||||
Hide();
|
Hide();
|
||||||
//_progressBar.Hide();
|
//_progressBar.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowCue(Ball CUEBALL)
|
public void Don(Ball CUEBALL)
|
||||||
{
|
{
|
||||||
_shown = true;
|
_donned = true;
|
||||||
SetProcess(true);
|
|
||||||
Position = CUEBALL.Position;
|
Position = CUEBALL.Position;
|
||||||
//_progressBar.Position = new Vector2(CUEBALL.Position.X - _progressBar.Size.X / 2, CUEBALL.Position.Y + _progressBar.Size.Y / 2);
|
SetProcess(true);
|
||||||
Show();
|
Show();
|
||||||
|
//_progressBar.Position = new Vector2(CUEBALL.Position.X - _progressBar.Size.X / 2, CUEBALL.Position.Y + _progressBar.Size.Y / 2);
|
||||||
//_progressBar.Show();
|
//_progressBar.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,11 +59,41 @@ move_down={
|
|||||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
space={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
left_click={
|
left_click={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
right_click={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
scroll_up={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":4,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
scroll_down={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ctrl={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
shift={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user