using System;
using System.Collections;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace Controllers
{
///
/// The controller for the basketball-game logic.
///
public class GameController : MonoBehaviour
{
private State state = State.JumpBall; // A basketball game always starts with a jump ball.
internal Animator BallAnimation;
public Player player;
public Player enemy;
public bool freezeMotion;
private float startTime;
public static float timeLimit;
///
/// The single ball for the game.
///
[SerializeField] public Ball ball;
[SerializeField] private float dribbleHeight;
[SerializeField] private float dribbleSpeed;
private Vector3 ballTarget;
[SerializeField] public SpriteRenderer[] trenchCoatSegments;
[Header("Spawn Points")]
[SerializeField] private SpawnPoints PlayerSpawnPoints;
[SerializeField] private SpawnPoints EnemySpawnPoints;
[Header("Hoops")]
[SerializeField] public Hoop PlayerHoop;
[SerializeField] public Hoop EnemyHoop;
[Header("SFX")]
[SerializeField] public AudioSource dribbleSound;
[SerializeField] public AudioSource airhornSound;
[Header("VFX")]
[SerializeField] private GameObject twoPointVFX;
[SerializeField] private GameObject threePointVFX;
[SerializeField] private GameObject spotlightVFX;
[Header("UI")]
[SerializeField] private Text playerScoreText;
[SerializeField] private Text enemyScoreText;
[SerializeField] private Text timerText;
[SerializeField] private GameObject resultOverlay;
[SerializeField] private Text resultText;
[SerializeField] private GameObject actionsUI;
private void Awake()
{
player = new Player { isEnemy = false, controller = this };
enemy = new Player { isEnemy = true, controller = this };
ball.controller = this;
PlayerHoop.controller = this;
EnemyHoop.controller = this;
BallAnimation = ball.GetComponentInChildren();
}
private void Start()
{
startTime = Time.time - 2f;
freezeMotion = true;
StartCoroutine(FadeInCoat());
}
private IEnumerator FadeInCoat()
{
foreach (var trenchCoat in trenchCoatSegments)
trenchCoat.material.color = new Color(1, 1, 1, 0);
yield return new WaitForSeconds(1f);
for (float t = 0; t < 1f; t += Time.deltaTime / 1)
{
foreach (var trenchCoat in trenchCoatSegments)
trenchCoat.material.color = new Color(1, 1, 1, Mathf.Lerp(0, 1, t));
yield return null;
}
yield return new WaitForSeconds(1f);
freezeMotion = false;
startTime = Time.time;
ball.Rigidbody.velocity = new Vector2(0, 10f);
}
private void Update()
{
UpdateUI();
}
private void FixedUpdate()
{
if (player.HasBall || enemy.HasBall)
{
ball.transform.position = ballTarget - new Vector3(0, (Mathf.Sin(Time.time * dribbleSpeed) + 1f) * (ballTarget.y - 0.75f) * dribbleHeight, 0);
}
}
private bool gameover;
private void UpdateUI()
{
playerScoreText.text = $"{player.score}";
enemyScoreText.text = $"{enemy.score}";
var remainingRaw = timeLimit - (Time.time - startTime);
var remaining = TimeSpan.FromSeconds(Mathf.Clamp(remainingRaw, 0, float.MaxValue));
timerText.text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
if (remainingRaw <= 0 && !gameover)
{
airhornSound.Play();
var outcome = player.score == enemy.score ? "TIE GAME" : player.score < enemy.score ? "AWAY TEAM WINS" : "HOME TEAM WINS";
actionsUI.SetActive(true);
ShowModal($"{outcome}\n{player.score}-{enemy.score}");
freezeMotion = true;
gameover = true;
}
}
public struct Player
{
///
/// Whether this player is the AI-enemy.
///
internal bool isEnemy;
///
/// A back-reference to the containing GameController.
///
internal GameController controller;
internal int score;
private Vector2 lastShotPosition;
public void Score(Vector2 Rim)
{
if (Vector2.Distance(lastShotPosition, Rim) >= 10)
{
score += 3;
controller.ParticleEffect(true, hoop);
}
else
{
score += 2;
controller.ParticleEffect(false, hoop);
}
// Make two spotlights.
Instantiate(controller.spotlightVFX);
Instantiate(controller.spotlightVFX);
// They made a shot! Now respawn the players and give possession to the opposite player.
controller.StartCoroutine(controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy, $"{controller.player.score}-{controller.enemy.score}"));
}
private State dribble => isEnemy ? State.EnemyDribble : State.PlayerDribble;
private State shoot => isEnemy ? State.EnemyShoot : State.PlayerShoot;
private Hoop hoop => isEnemy ? controller.EnemyHoop : controller.PlayerHoop;
public bool HasBall => controller.state == dribble;
public bool IsShooting => controller.state == shoot;
///
/// When dribbling, move the ball with the player.
///
/// The position of the hand dribbling the ball.
public void Move(Vector2 handPosition)
{
if (controller.state == (isEnemy ? State.EnemyDribble : State.PlayerDribble)) // Make sure they're dribbling.
controller.ballTarget = handPosition;
}
///
/// Grab the ball if possible given the current game state.
///
/// The position of the hand to attempt grabbing from.
/// Whether or not the ball was able to be picked up.
public bool GrabBall(Vector2 handPosition)
{
// 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.
if (controller.state == shoot || controller.state == dribble) return false;
// Make sure its within their grab area.
if (Vector2.Distance(controller.ball.transform.position, handPosition) >= 1f) return false;
controller.state = dribble;
controller.BallAnimation.enabled = false;
controller.dribbleSound.Play();
Move(handPosition);
controller.ball.Rigidbody.bodyType = RigidbodyType2D.Kinematic;
return true;
}
///
/// Shoot the ball if possible.
///
///
///
/// Whether or not the ball was shot
public bool Shoot(Transform playerTransform, float time)
{
if (controller.state != dribble) return false; // We must be dribbling the ball to shoot it.
controller.BallAnimation.enabled = true;
controller.dribbleSound.Stop();
controller.state = shoot;
controller.ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
controller.ball.Shoot(hoop, time);
lastShotPosition = playerTransform.position;
return true;
}
public void Foul(string reason)
{
// Give the other player the ball on a foul.
controller.StartCoroutine(controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy, reason, false));
}
}
internal void BallDropped()
{
BallAnimation.enabled = true;
dribbleSound.Stop();
ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
state = State.Idle;
}
private void ParticleEffect(bool threePts, Hoop hoop)
{
var vfx = Instantiate(threePts ? threePointVFX : twoPointVFX);
vfx.transform.position = hoop.transform.position;
}
private IEnumerator Respawn(Possession possession, string message, bool wait = true)
{
BallDropped();
yield return new WaitForSeconds(wait ? 0.5f : 0f); // Wait a slight bit before respawning so they can see the VFXs.
PlayerSpawnPoints.body.transform.position = new Vector3(PlayerSpawnPoints.character.position.x, PlayerSpawnPoints.character.position.y, PlayerSpawnPoints.body.transform.position.y);
PlayerSpawnPoints.body.GetComponent().velocity = Vector2.zero;
if (PlayerSpawnPoints.secondBody is { })
{
if (PlayerSpawnPoints.secondCharacter is { })
PlayerSpawnPoints.secondBody.transform.position = new Vector3(PlayerSpawnPoints.secondCharacter.position.x, PlayerSpawnPoints.secondCharacter.position.y, PlayerSpawnPoints.body.transform.position.y);
PlayerSpawnPoints.secondBody.GetComponent().velocity = Vector2.zero;
}
PlayerSpawnPoints.body.transform.localRotation = Quaternion.identity;
EnemySpawnPoints.body.transform.position = new Vector3(EnemySpawnPoints.character.position.x, EnemySpawnPoints.character.position.y, EnemySpawnPoints.body.transform.position.y);
ball.transform.position = possession switch
{
Possession.Player => new Vector3(PlayerSpawnPoints.ball.position.x, PlayerSpawnPoints.ball.position.y, ball.transform.position.y),
Possession.Enemy => new Vector3(EnemySpawnPoints.ball.position.x, EnemySpawnPoints.ball.position.y, ball.transform.position.y),
_ => ball.transform.position
};
// Set a cooldown so they can stop trying to wrangle the player while it respawns.
StartCoroutine(RespawnCooldown(possession, message));
}
private IEnumerator RespawnCooldown(Possession possession, string message)
{
// Show the new score.
var possessionText = possession == Possession.Player ? "HOME" : "AWAY";
ShowModal($"{message}\n{possessionText}'S POSSESSION");
freezeMotion = true;
yield return new WaitForSeconds(1f);
HideModal();
freezeMotion = false;
}
private void ShowModal(string text)
{
if (gameover) return;
resultOverlay.SetActive(true);
resultText.text = text;
}
private void HideModal()
{
if (gameover) return;
resultOverlay.SetActive(false);
}
public void Restart() => SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
public void MainMenu() => SceneManager.LoadScene("Menu");
internal enum State
{
Idle,
JumpBall,
PlayerDribble,
PlayerShoot,
EnemyDribble,
EnemyShoot,
}
[Serializable]
private struct SpawnPoints
{
[SerializeField] internal Transform body;
[SerializeField] [CanBeNull] internal Transform secondBody;
[SerializeField] internal Transform ball;
[SerializeField] internal Transform character;
[SerializeField] [CanBeNull] internal Transform secondCharacter;
}
private enum Possession
{
Player,
Enemy
}
}
internal static class GameControllerStateExtensions
{
internal static bool IsShot(this GameController.State state)
{
return state == GameController.State.EnemyShoot || state == GameController.State.PlayerShoot;
}
internal static bool IsDribble(this GameController.State state)
{
return state == GameController.State.EnemyDribble || state == GameController.State.PlayerDribble;
}
}
}