2 using System.Collections;
3 using JetBrains.Annotations;
10 /// The controller for the basketball-game logic.
12 public class GameController : MonoBehaviour
14 private State state = State.JumpBall; // A basketball game always starts with a jump ball.
15 internal Animator BallAnimation;
20 public bool freezeMotion;
22 private float startTime;
23 [SerializeField] private float timeLimit;
26 /// The single ball for the game.
28 [SerializeField] public Ball ball;
30 [Header("Spawn Points")]
31 [SerializeField] private SpawnPoints PlayerSpawnPoints;
32 [SerializeField] private SpawnPoints EnemySpawnPoints;
35 [SerializeField] public Hoop PlayerHoop;
36 [SerializeField] public Hoop EnemyHoop;
39 [SerializeField] public AudioSource dribbleSound;
42 [SerializeField] private Text playerScoreText;
43 [SerializeField] private Text enemyScoreText;
44 [SerializeField] private Text timerText;
46 [SerializeField] private GameObject resultOverlay;
47 [SerializeField] private Text resultText;
51 player = new Player { isEnemy = false, controller = this };
52 enemy = new Player { isEnemy = true, controller = this };
53 ball.controller = this;
54 PlayerHoop.controller = this;
55 EnemyHoop.controller = this;
56 BallAnimation = ball.GetComponentInChildren<Animator>();
61 startTime = Time.time;
69 private void UpdateUI()
71 playerScoreText.text = $"{player.score}";
72 enemyScoreText.text = $"{enemy.score}";
74 var remainingRaw = timeLimit - (Time.time - startTime);
75 var remaining = TimeSpan.FromSeconds(Mathf.Clamp(remainingRaw, 0, float.MaxValue));
76 timerText.text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
78 if (remainingRaw <= 0)
80 resultOverlay.SetActive(true);
81 var outcome = player.score == enemy.score ? "TIE GAME" : player.score < enemy.score ? "AWAY TEAM WINS" : "HOME TEAM WINS";
82 resultText.text = $"{outcome}\n{player.score}-{enemy.score}";
89 /// Whether this player is the AI-enemy.
91 internal bool isEnemy;
94 /// A back-reference to the containing GameController.
96 internal GameController controller;
100 private Vector2 lastShotPosition;
101 public void Score(Vector2 Rim)
103 if (Vector2.Distance(lastShotPosition, Rim) >= 10)
112 // They made a shot! Now respawn the players and give possession to the opposite player.
113 controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy);
116 private State dribble => isEnemy ? State.EnemyDribble : State.PlayerDribble;
117 private State shoot => isEnemy ? State.EnemyShoot : State.PlayerShoot;
119 private Hoop hoop => isEnemy ? controller.EnemyHoop : controller.PlayerHoop;
121 public bool HasBall => controller.state == dribble;
124 /// When dribbling, move the ball with the player.
126 /// <param name="handPosition">The position of the hand dribbling the ball.</param>
127 public void Move(Vector2 handPosition)
129 if (controller.state == (isEnemy ? State.EnemyDribble : State.PlayerDribble)) // Make sure they're dribbling.
130 controller.ball.transform.position = handPosition; // TODO: Make this perform a dribbling motion, otherwise it looks like they're travelling.
134 /// Grab the ball if possible given the current game state.
136 /// <param name="handPosition">The position of the hand to attempt grabbing from.</param>
137 /// <returns>Whether or not the ball was able to be picked up.</returns>
138 public bool GrabBall(Vector2 handPosition)
140 // Don't allow the ball to be picked up if someone shot it. Also don't try picking it up if we're already holding it.
141 if (controller.state == shoot || controller.state == dribble) return false;
143 // Make sure its within their grab area.
144 if (Vector2.Distance(controller.ball.transform.position, handPosition) >= 1f) return false;
146 controller.state = dribble;
147 controller.BallAnimation.enabled = false;
148 controller.dribbleSound.Play();
151 controller.ball.Rigidbody.bodyType = RigidbodyType2D.Kinematic;
157 /// Shoot the ball if possible.
159 /// <param name="playerTransform"></param>
160 /// <param name="time"></param>
161 /// <returns>Whether or not the ball was shot</returns>
162 public bool Shoot(Transform playerTransform, float time)
164 if (controller.state != dribble) return false; // We must be dribbling the ball to shoot it.
165 controller.BallAnimation.enabled = true;
166 controller.dribbleSound.Stop();
167 controller.state = shoot;
168 controller.ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
169 controller.ball.Shoot(hoop, time);
170 lastShotPosition = playerTransform.position;
175 internal void BallDropped()
177 BallAnimation.enabled = true;
179 ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
183 private void Respawn(Possession possession)
185 PlayerSpawnPoints.body.transform.position = new Vector3(PlayerSpawnPoints.character.position.x, PlayerSpawnPoints.character.position.y, PlayerSpawnPoints.body.transform.position.y);
186 PlayerSpawnPoints.body.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
188 if (PlayerSpawnPoints.secondBody is { })
190 if (PlayerSpawnPoints.secondCharacter is { })
191 PlayerSpawnPoints.secondBody.transform.position = new Vector3(PlayerSpawnPoints.secondCharacter.position.x, PlayerSpawnPoints.secondCharacter.position.y, PlayerSpawnPoints.body.transform.position.y);
192 PlayerSpawnPoints.secondBody.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
195 PlayerSpawnPoints.body.transform.localRotation = Quaternion.identity;
196 EnemySpawnPoints.body.transform.position = new Vector3(EnemySpawnPoints.character.position.x, EnemySpawnPoints.character.position.y, EnemySpawnPoints.body.transform.position.y);
197 ball.transform.position = possession switch
199 Possession.Player => new Vector3(PlayerSpawnPoints.ball.position.x, PlayerSpawnPoints.ball.position.y, ball.transform.position.y),
200 Possession.Enemy => new Vector3(EnemySpawnPoints.ball.position.x, EnemySpawnPoints.ball.position.y, ball.transform.position.y),
201 _ => ball.transform.position
204 // Set a cooldown so they can stop trying to wrangle the player while it respawns.
205 StartCoroutine(RespawnCooldown(possession));
208 private IEnumerator RespawnCooldown(Possession possession)
210 // Show the new score.
211 var possessionText = possession == Possession.Player ? "HOME" : "AWAY";
212 ShowModal($"{player.score}-{enemy.score}\n{possessionText}'S POSSESSION");
216 yield return new WaitForSeconds(1f);
220 freezeMotion = false;
223 private void ShowModal(string text)
225 resultOverlay.SetActive(true);
226 resultText.text = text;
229 private void HideModal() => resultOverlay.SetActive(false);
242 private struct SpawnPoints
244 [SerializeField] internal Transform body;
245 [SerializeField] [CanBeNull] internal Transform secondBody;
246 [SerializeField] internal Transform ball;
247 [SerializeField] internal Transform character;
248 [SerializeField] [CanBeNull] internal Transform secondCharacter;
251 private enum Possession
258 internal static class GameControllerStateExtensions
260 internal static bool IsShot(this GameController.State state)
262 return state == GameController.State.EnemyShoot || state == GameController.State.PlayerShoot;
265 internal static bool IsDribble(this GameController.State state)
267 return state == GameController.State.EnemyDribble || state == GameController.State.PlayerDribble;