using DigitalRuby.Tween;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
///
/// Contains all game logic for the SpellingBee game
///
public partial class SpellingBeeController : AbstractMinigameController
{
///
/// All of the words that can be used in this session
///
private List words = new List();
///
/// Where we currently are in the word
///
private int letterIndex;
///
/// Where we currently are in the word list
///
private int wordIndex;
///
/// The word that is currently being spelled
///
private string currentWord;
///
/// All of the available themes
///
public ThemeList themeList;
///
/// The theme we are currently using
///
private Theme currentTheme;
///
/// Current value of timer in seconds
///
private float timerValue;
///
/// List of learnables to get the threshold for the letters
///
public Theme fingerspelling;
///
/// Amount of seconds user gets per letter of the current word
/// Set to 1 for testing; should be increased later
///
private const int secondsPerLetter = 5;
///
/// Counter that keeps track of how many letters have been spelled correctly
///
private int correctLetters;
///
/// Counter that keeps track of how many letters have been spelled incorrectly
///
private int incorrectLetters;
///
/// Counter that keeps track of how many words have been spelled correctly
///
private int spelledWords;
///
/// Timer that keeps track of when the game was started
///
private DateTime startTime;
///
/// Letter prefab
///
public GameObject letterPrefab;
///
/// Reference to letter container
///
public Transform letterContainer;
///
/// The Image component for displaying the appropriate sprite
///
public Image wordImage;
///
/// Timer display
///
public TMP_Text timerText;
///
/// Bonus time display
///
public GameObject bonusTimeText;
///
/// Timer to display the bonus time
///
private float bonusActiveRemaining = 0.0f;
///
/// The GameObjects representing the letters
///
private List letters = new List();
///
/// Reference to the scoreboard
///
public Transform Scoreboard;
///
/// Reference to the feedback field
///
public TMP_Text feedbackText;
///
/// Reference to the progress bar image, so we can add fancy colors
///
public Image feedbackProgressImage;
///
/// Timer to keep track of how long a incorrect sign is performed
///
protected DateTime timer;
///
/// Current predicted sign
///
protected string predictedSign = null;
///
/// Previous incorrect sign, so we can keep track whether the user is wrong or the user is still changing signs
///
protected string previousIncorrectSign = null;
///
/// Reference to display the score
///
public TMP_Text scoreDisplay;
///
/// Reference to display the points lost/won
///
public TMP_Text scoreBonus;
///
/// Score obtained when spelling a letter
///
private const int correctLettersScore = 10;
///
/// Score obtained when spelling the wrong letter :o
///
private const int incorrectLettersScore = -5;
///
/// Set the AbstractMinigameController variable to inform it of the theme for the signPredictor
///
protected override Theme signPredictorTheme
{
get { return fingerspelling; }
}
///
/// Timer to keep track of how long a sign is performed
///
protected DateTime acceptance_test_timer;
///
/// Update is called once per frame
///
public void Update()
{
if (gameIsActive)
{
timerValue -= Time.deltaTime;
if (bonusActiveRemaining <= 0.0 && bonusTimeText.activeSelf)
{
bonusTimeText.SetActive(false);
scoreBonus.text = "";
}
else
{
bonusActiveRemaining -= Time.deltaTime;
}
if (timerValue <= 0.0f)
{
timerValue = 0.0f;
//ActivateGameOver();
ActivateEnd(false);
}
int minutes = Mathf.FloorToInt(timerValue / 60.0f);
int seconds = Mathf.FloorToInt(timerValue % 60.0f);
timerText.text = string.Format("{0:00}:{1:00}", minutes, seconds);
}
}
///
/// Randomly shuffle the list of words
///
public void ShuffleWords()
{
for (int i = words.Count - 1; i > 0; i--)
{
// Generate a random index between 0 and i (inclusive)
int j = UnityEngine.Random.Range(0, i + 1);
// Swap the values at indices i and j
(words[j], words[i]) = (words[i], words[j]);
}
}
///
/// Calculate the score
///
/// The calculated score
public override int CalculateScore()
{
return correctLetters * correctLettersScore + incorrectLetters * incorrectLettersScore;
}
///
/// Delete all letter objects
///
public void DeleteWord()
{
for (int i = 0; i < letters.Count; i++)
{
Destroy(letters[i]);
}
letters.Clear();
}
///
/// Adds seconds to timer
///
///
public void AddSeconds(int seconds)
{
timerValue += (float)seconds;
bonusTimeText.SetActive(true);
bonusActiveRemaining = 1.0f;
}
///
/// Display the next letter
///
/// true if the letter was correctly signed, false otherwise
public void NextLetter(bool successful)
{
if (!gameIsActive) { return; }
// Change color of current letter (skip spaces)
if (successful)
{
correctLetters++;
letters[letterIndex].GetComponent().color = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f);
scoreDisplay.text = $"Score: {CalculateScore()}";
scoreBonus.text = $"+{correctLettersScore}";
scoreBonus.color = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f);
}
else
{
incorrectLetters++;
letters[letterIndex].GetComponent().color = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
scoreDisplay.text = $"Score: {CalculateScore()}";
scoreBonus.text = $"{incorrectLettersScore}";
scoreBonus.color = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
}
do
{
letterIndex++;
} while (letterIndex < currentWord.Length && currentWord[letterIndex] == ' ');
acceptance_test_timer = DateTime.Now;
// Change the color of the next letter or change to new word
if (letterIndex < currentWord.Length)
{
letters[letterIndex].GetComponent().color = new Color(0x9f / 255.0f, 0xe7 / 255.0f, 0xf5 / 255.0f);
}
else
{
StartCoroutine(Wait());
NextWord();
}
}
///
/// Display next word in the series
///
public void NextWord()
{
DeleteWord();
spelledWords++;
if (wordIndex < words.Count)
{
currentWord = words[wordIndex].name;
letterIndex = 0;
DisplayWord(currentWord);
wordIndex++;
}
else
{
ActivateEnd(true);
}
}
///
/// Displays the word that needs to be spelled
///
/// The word to display
public void DisplayWord(string word)
{
for (int i = 0; i < word.Length; i++)
{
// Create instance of prefab
GameObject instance = GameObject.Instantiate(letterPrefab, letterContainer);
letters.Add(instance);
// Dynamically load appearance
char c = Char.ToUpper(word[i]);
Image background = instance.GetComponent();
background.color = i == 0 ? new Color(0x9f / 255.0f, 0xe7 / 255.0f, 0xf5 / 255.0f) : Color.clear;
TMP_Text txt = instance.GetComponentInChildren();
txt.text = Char.ToString(c);
}
wordImage.sprite = words[wordIndex].image;
}
///
/// Wait for 2 seconds
///
private IEnumerator Wait()
{
yield return new WaitForSecondsRealtime(2);
}
///
/// Get the threshold for a given sign
///
///
///
public float GetThreshold(string sign)
{
Learnable letter = fingerspelling.learnables.Find(l => l.name == sign);
return letter.thresholdDistance;
}
///
/// Function to get the current letter that needs to be signed
///
/// the current letter that needs to be signed
public string GetSign()
{
if (letterIndex < currentWord.Length)
{
return currentWord[letterIndex].ToString().ToUpper();
}
return null;
}
///
/// Function to confirm your prediction and check if it is correct.
///
///
public void PredictSign(string sign)
{
bool successful = sign.ToUpper() == currentWord[letterIndex].ToString().ToUpper();
if (successful)
{
// Timer acceptance test
Debug.Log(DateTime.Now - acceptance_test_timer);
AddSeconds(secondsPerLetter);
}
NextLetter(successful);
}
///
/// The logic to process the signs sent by the signPredictor
///
/// The accuracy of the passed sign
/// The name of the passed sign
public override void ProcessMostProbableSign(float distance, string predictedSign)
{
string currentSign = GetSign();
float distCurrentSign = signPredictor.learnableProbabilities[currentSign];
ProcessCurrentAndPredicted(distance, predictedSign, distCurrentSign, currentSign);
}
///
/// The logic to process the current en predicted sign by the signPredictor
///
/// The accuracy of the passed sign
/// The name of the passed sign
public void ProcessCurrentAndPredicted(float distPredictSign, string predictedSign, float distCurrentSign, string currentSign)
{
float thresholdCurrentSign = GetThreshold(currentSign);
float thresholdPredictedSign = GetThreshold(predictedSign);
// If there is a feedback-object, we wil change its appearance
if (feedbackText != null && feedbackProgressImage != null)
{
Color col;
if (distCurrentSign < thresholdCurrentSign)
{
feedbackText.text = "Goed";
col = new Color(0x8b / 255.0f, 0xd4 / 255.0f, 0x5e / 255.0f);
}
else if (distCurrentSign < 1.5 * thresholdCurrentSign)
{
feedbackText.text = "Bijna...";
col = new Color(0xf2 / 255.0f, 0x7f / 255.0f, 0x0c / 255.0f);
}
else if (distPredictSign < thresholdPredictedSign)
{
feedbackText.text = $"Verkeerde gebaar: '{predictedSign}'";
col = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
}
else
{
feedbackText.text = "Detecteren...";
col = new Color(0xf5 / 255.0f, 0x49 / 255.0f, 0x3d / 255.0f);
}
feedbackText.color = col;
feedbackProgressImage.color = col;
float oldValue = feedbackProgress.value;
// use an exponential scale
float newValue = 1 - Mathf.Clamp(distCurrentSign - thresholdCurrentSign, 0.0f, 3.0f) / 3;
feedbackProgress.gameObject.Tween("FeedbackUpdate", oldValue, newValue, 0.2f, TweenScaleFunctions.CubicEaseInOut, (t) =>
{
if (feedbackProgress != null)
{
feedbackProgress.value = t.CurrentValue;
}
});
}
// The logic for the internal workings of the game
if (distPredictSign < thresholdPredictedSign)
{
// Correct sign, instantly pass it along
if (predictedSign == currentSign)
{
PredictSign(predictedSign);
timer = DateTime.Now;
predictedSign = null;
previousIncorrectSign = null;
}
// Incorrect sign, wait a bit before passing it along
else
{
if (previousIncorrectSign != predictedSign)
{
timer = DateTime.Now;
previousIncorrectSign = predictedSign;
}
else if (DateTime.Now - timer > TimeSpan.FromSeconds(2.0f))
{
PredictSign(predictedSign);
timer = DateTime.Now;
predictedSign = null;
previousIncorrectSign = null;
}
}
}
}
///
/// The logic to set the scoreboard of spellingbee
///
/// SHows whether or not the player won
protected override void SetScoreBoard(bool victory)
{
string resultTxt;
if (victory)
{
resultTxt = "GEWONNEN";
}
else
{
resultTxt = "VERLOREN";
}
// Save the scores and show the scoreboard
gameEndedPanel.GetComponent().GenerateContent(
startTime: startTime,
totalWords: spelledWords,
correctLetters: correctLetters,
incorrectLetters: incorrectLetters,
result: resultTxt,
score: CalculateScore()
);
}
///
/// The spellinbee-specific logic that needs to be called at the start of the game
///
protected override void StartGameLogic()
{
correctLetters = 0;
incorrectLetters = 0;
words.Clear();
// We use -1 instead of 0 so SetNextWord can simply increment it each time
spelledWords = -1;
wordIndex = 0;
gameIsActive = true;
timerValue = 30.0f;
bonusActiveRemaining = 0.0f;
startTime = DateTime.Now;
gameEndedPanel.SetActive(false);
bonusTimeText.SetActive(false);
currentTheme = minigame.themeList.themes[minigame.themeList.currentThemeIndex];
words.AddRange(currentTheme.learnables);
ShuffleWords();
NextWord();
scoreDisplay.text = $"Score: {CalculateScore()}";
scoreBonus.text = "";
// timer for acceptance test
acceptance_test_timer = DateTime.Now;
}
///
/// The spellingbee-specific logic that needs to be called at the end of a game
///
///
protected override void EndGameLogic(bool victory)
{
gameIsActive = false;
DeleteWord();
}
///
/// Skip to ending of game, used in testing to see if NextWord(), NextLetter() and ScoreBord work well together
///
///
public string SkipToEnd()
{
wordIndex = words.Count - 1;
currentWord = words[wordIndex].name;
return currentWord;
}
}