using Microsoft.VisualStudio.Utilities; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.IO; using UnityEngine; using UnityEngine.Serialization; using Utility; using Random = UnityEngine.Random; [Serializable] public struct DeveloperStats { public double BaseEfficiency; public int Fingers; public double CaffeineDrainFactor; public double HungerDrainFactor; public double UrinationDrainFactor; public double HappinessDrainFactor; // TODO: Not yet used public double CoffeePreference; public double MatePreference; public DeveloperStats(double baseEfficiency, int fingers, double caffeineDrainFactor, double hungerDrainFactor, double urinationDrainFactor, double happinessDrainFactor, double coffeePreference, double matePreference) { BaseEfficiency = baseEfficiency; Fingers = fingers; CaffeineDrainFactor = caffeineDrainFactor; HungerDrainFactor = hungerDrainFactor; UrinationDrainFactor = urinationDrainFactor; HappinessDrainFactor = happinessDrainFactor; CoffeePreference = coffeePreference; MatePreference = matePreference; } public static readonly DeveloperStats Default = new DeveloperStats(1.0, 10, 1, 1, 1, 1, 0.5, 0.5); } public class Developer : MonoBehaviour { [Header("Basis Informationen")] [SerializeField] private string _name; [SerializeField] private DeveloperStats _baseStats = DeveloperStats.Default; [SerializeField] private float _talkRange = 2.0f; [Header("Aktuelle Informationen")] [SerializeField, ShowOnly] private double _currentEfficiency = 1.0; [SerializeField] private int _fingersLeft = 10; [SerializeField] private double _caffeineLevel = 1.0; [SerializeField] private double _hungerLevel = 1.0; [SerializeField] private double _urgeToUrinateLevel = 1.0; [SerializeField] private double _happiness = 0.75; [SerializeField, ShowOnly] private bool _isSleeping = false; [SerializeField, ShowOnly] private bool _isHyperactive = false; [SerializeField, ShowOnly] private bool _isOvercaffeinated = false; [SerializeField] private DeveloperNeeds _developerNeeds; /// /// Gibt die Grunddaten des Entwicklers zurück. /// public DeveloperStats BaseStats => _baseStats; /// /// Gibt die Anzahl der Finger zurück. /// public int FingersLeft => _fingersLeft; /// /// Gibt die aktuelle Effizienz des Entwicklers in Prozent zurück. /// public double CurrentEfficiency => _currentEfficiency; public string Name => _name; [SerializeField] private GameObject _caffeineNeed; [SerializeField] private WantedConsumable _wantedDrink; [SerializeField] private GameObject _hungerNeed; [SerializeField] private WantedConsumable _wantedFood; [SerializeField] private GameObject _toiletNeed; private List _needList = new List(); private bool _isDead = false; /// /// indicates wether the Dev is dead or not /// public bool IsDead => _isDead; [SerializeField] private int _maxPrivateContextBufferSize = 2; [SerializeField] private CircularBuffer _privateContextBuffer; /// /// Returns the private Context Buffer /// public CircularBuffer PrivateContextBuffer => _privateContextBuffer; // stuff for talking [SerializeField, ShowOnly] private float _talkTimer; private AudioSource _audioSource; private float _talkPauseTime = 15.0f; private bool _hasTalkedWhileHyperactive = false; private bool _hasTalkedWhileOvercaffeinated = false; private bool _hasTalkedBeforeSleeping = false; void Start() { _developerNeeds = gameObject.GetComponent(); _audioSource = GetComponent(); _talkTimer = Random.Range(5.0f, 15.0f); _fingersLeft = _baseStats.Fingers; _privateContextBuffer = new CircularBuffer(_maxPrivateContextBufferSize); } private void Update() { if (!_audioSource.isPlaying) { _talkTimer -= Time.deltaTime; if (_talkTimer < 0.0f) { _talkTimer = _talkPauseTime; TalkIfInRange(); } } } [ContextMenu("Hurt him")] private void TestHurt() { Hurt(); } [ContextMenu("Give Coffee")] private void TestCoffee() { GiveDrink(0.3, WantedConsumable.Coffee); } [ContextMenu("Give Mate")] private void TestMate() { GiveDrink(0.3, WantedConsumable.Mate); } [ContextMenu("Give Food")] private void TestFood() { GiveFood(0.3, WantedConsumable.Pizza); } [ContextMenu("Drain Bladder")] private void TestPee() { Pee(0.3, true); } public void GiveDrink(double caffeineAmount, WantedConsumable drinkType) { if (!drinkType.HasFlag(WantedConsumable.Drink)) throw new ArgumentException(nameof(drinkType), $"{nameof(drinkType)} must be a value with the \"{WantedConsumable.Drink}\" flag"); _caffeineLevel = Math.Min(_caffeineLevel + caffeineAmount, 2.0); if (_caffeineNeed != null && _caffeineLevel > GameManager.Instance.NeedNotificationThreshold) { UpdateNeedPositions(_caffeineNeed); _needList.Remove(_caffeineNeed); NeedFullfilled(_caffeineNeed); _caffeineNeed = null; } if (_wantedDrink != WantedConsumable.None) { // TODO: Wie wäre es damit, das nicht fest zu coden? if (drinkType == _wantedDrink) { Talk($"The Developer thanks Gottfried for the {drinkType.GetAsString()}", 1); _privateContextBuffer.Add($"The Developer is greatful for the {drinkType.GetAsString()} Gottfried brought him a while ago"); _happiness += 0.2; } else { Talk($"The Developer is happy about the caffeine but he blames Gottfried for bringing {drinkType.GetAsString()} because he actaully wanted a {_wantedDrink.GetAsString()} instead", 0, true); _privateContextBuffer.Add($"The Developer is still annoyed and reminds Gottfried that he brought him {drinkType.GetAsString()} instead of {_wantedDrink.GetAsString()} a while ago"); _happiness -= 0.2; } } else { _happiness += 0.2; } _wantedDrink = WantedConsumable.None; } public void GiveFood(double foodAmount, WantedConsumable foodType) { if (!foodType.HasFlag(WantedConsumable.Food)) throw new ArgumentException(nameof(foodType), $"{nameof(foodType)} must be a value with the \"{WantedConsumable.Food}\" flag"); _hungerLevel = Math.Min(_hungerLevel + foodAmount, 1.0); if (_hungerNeed != null && _hungerLevel > GameManager.Instance.NeedNotificationThreshold) { UpdateNeedPositions(_hungerNeed); _needList.Remove(_hungerNeed); NeedFullfilled(_hungerNeed); _hungerNeed = null; Talk("The Developer thanks Gottfried for the Pizza", 1); _privateContextBuffer.Add($"The Developer is greatful for the Pizza Gottfried brought him a while ago"); } if (_wantedFood != WantedConsumable.None) { if (foodType == _wantedFood) _happiness += 0.2; else _happiness -= 0.2; } else { _happiness += 0.2; } _wantedFood = WantedConsumable.None; } /// /// Der Entwickler Pinkelt die angegebene Menge aus. /// /// Die Menge die ausgepinkelt wird. /// Wenn true, dann pinkelt der Entwickler auf einer Toilette. Wenn false, dann pinkelt er auf den Boden (was schlecht ist) public void Pee(double peeAmount, bool onToilet) { _urgeToUrinateLevel = Math.Min(_urgeToUrinateLevel += peeAmount, 1.0); if (_toiletNeed != null && _urgeToUrinateLevel > GameManager.Instance.NeedNotificationThreshold) { UpdateNeedPositions(_toiletNeed); _needList.Remove(_toiletNeed); NeedFullfilled(_toiletNeed); _toiletNeed = null; Talk("The Developer finally can go to the toilet", 1); _privateContextBuffer.Add($"The developer is grateful that he went to the toilet a while ago"); } if (onToilet) _happiness += 0.2; else _happiness -= 0.2; } public void UpdateStats(double caffeineDrain, double hungerDrain, double urinationDrain, double happinessDrain) { _caffeineLevel -= caffeineDrain * _baseStats.CaffeineDrainFactor; _hungerLevel -= hungerDrain * _baseStats.HungerDrainFactor; _urgeToUrinateLevel -= urinationDrain * _baseStats.UrinationDrainFactor; _happiness -= happinessDrain * _baseStats.UrinationDrainFactor; _caffeineLevel = Math.Max(_caffeineLevel, 0.0); _hungerLevel = Math.Max(_hungerLevel, 0.0); _urgeToUrinateLevel = Math.Max(_urgeToUrinateLevel, 0.0); _happiness = Math.Max(_happiness, 0.0); _isHyperactive = _caffeineLevel > 1.0 && _caffeineLevel <= 1.5; _isOvercaffeinated = _caffeineLevel > 1.5; _isSleeping = _caffeineLevel <= 0.0; if (_caffeineLevel < GameManager.Instance.NeedNotificationThreshold && _caffeineNeed == null) { // TODO: Wir können hier anhand von Präferenz gewichten. if (Random.Range(0.0f, 1.0f) > 0.5f) { _caffeineNeed = _developerNeeds.SpawnMateNeed(_needList.Count); _wantedDrink = WantedConsumable.Mate; _needList.Add(_caffeineNeed); } else { _caffeineNeed = _developerNeeds.SpawnCoffeeNeed(_needList.Count); _wantedDrink = WantedConsumable.Coffee; _needList.Add(_caffeineNeed); } } if (_hungerLevel < GameManager.Instance.NeedNotificationThreshold && _hungerNeed == null) { _hungerNeed = _developerNeeds.SpawnHungerNeed(_needList.Count); _wantedFood = WantedConsumable.Pizza; _needList.Add(_hungerNeed); } if (_urgeToUrinateLevel < GameManager.Instance.NeedNotificationThreshold && _toiletNeed == null) { // TODO: Go to toilet Debug.Log("Ich muss aufs Klo!"); _toiletNeed = _developerNeeds.SpawnToiletNeed(_needList.Count); _needList.Add(_toiletNeed); } if (_hungerLevel <= 0.0) { if (!_isDead) Talk($"The developer is starving, The developer is dying, The developer blames Gottfried for letting him starve with his last words", 0, true); Die(); } } private void UpdateNeedPositions(GameObject reference) { int referenceIndex = _needList.IndexOf(reference); if (referenceIndex == -1) { Debug.LogError("Reference GameObject not in List."); return; } for (int i = referenceIndex + 1; i < _needList.Count; i++) { GameObject currentItem = _needList[i]; currentItem.GetComponent().originalY -= 0.5f; } } private void NeedFullfilled(GameObject needObject) { Instantiate(GameManager.Instance.NeedFullfilledParticleEffect, needObject.transform.position, needObject.transform.rotation); Destroy(needObject); } public void UpdateEfficiency() { _currentEfficiency = _baseStats.BaseEfficiency * (_fingersLeft / 10.0) * CalculateCaffeineEfficiency() * CalculateHungerEfficiency() * CalculateUrinationEfficiency() * CalculateHappinessEfficiency(); } // TODO: Es könnte sich als schwierig erweisen, die Effizienz hoch zu halten. // Man könnte ggf. die Funktionen so anpassen, dass sie eine ganze Weile 100% Effizienz geben. // TODO: Das auch nur ansatzweise zu balancieren wird wiztig 🤯 private double CalculateCaffeineEfficiency() { if (_isSleeping) { if (!_hasTalkedBeforeSleeping) { Talk("The Developer is very sleepy right now duo to a lack of caffeine, The Developer takes a nap now", 0, true); _privateContextBuffer.Add($"The developer took a refreshing nap but is annoyed that Gottfried forgot to bring him any caffeine before the nap"); _hasTalkedBeforeSleeping = true; } return 0.0; } else { _hasTalkedBeforeSleeping = false; } if (_isHyperactive) { if (!_hasTalkedWhileHyperactive) { GetComponent().speakingSpeed = 1.4f; Talk("The Developer is surprisingly productive right now and feels an energyboost duo to caffeine", 0, true); _hasTalkedWhileHyperactive = true; } } else { GetComponent().speakingSpeed = 1.1f; _hasTalkedWhileHyperactive = false; } if (_isOvercaffeinated) { if (!_hasTalkedWhileOvercaffeinated) { Talk("The Developer had too much caffeine, The Developer needs a break immediately", 0, true); _privateContextBuffer.Add($"The developer overcaffeinated a while ago because Gottfried gave him too much caffeine"); _hasTalkedWhileOvercaffeinated = true; } return 0.0; } else { _hasTalkedWhileOvercaffeinated = false; } // https://easings.net/#easeOutCubic return 1.0 - Math.Pow(1.0 - _caffeineLevel, 3.0); } private double CalculateHungerEfficiency() { if (_hungerLevel > 1.0) return 1.0; // https://easings.net/#easeOutCirc return Math.Sqrt(1.0 - Math.Pow(_hungerLevel - 1.0, 2.0)); } private double CalculateUrinationEfficiency() { if (_urgeToUrinateLevel > 1.0) return 1.0; // https://easings.net/#easeOutExpo return Math.Abs(_urgeToUrinateLevel - 1.0) < 0.0001f ? 1.0 : 1.0 - Math.Pow(2, -10 * _urgeToUrinateLevel); } private double CalculateHappinessEfficiency() { // 50% bei 0 Happiness, 100% bei 0.75 Happiness, 125% bei 1 Happiness return 0.5 + _happiness * 2.0 / 3.0; } /// /// Der Entwickler wird verletzt und der Idiot bricht sich ausgerechnet einen Finger... /// public void Hurt() { _fingersLeft--; // Ob er stirbt oder nicht, für uns hat er auf jeden Fall seinen Nutzen verloren. if (_fingersLeft == 0) { if (!_isDead) Talk($"The developer lost all his fingers duo to an attack by a monster, The developer is dying, The developer gives a short speech with his last words, The developer blames Gottfried for his death", 0, true); Die(); } else { if (_fingersLeft == 9) Talk($"The developer lost a finger duo to an attack by a monster, The developer has {_fingersLeft} left, The developer is in pain, The developer is irrevocably less productive now, The developer blames Gottfried for this", 0, true); else Talk($"The developer lost another finger duo to an attack by a monster, The developer has only {_fingersLeft} left, The developer is in pain, The developer is irrevocably less productive now, The developer blames Gottfried for this", 0, true); if (Random.Range(0, 3) < 2) _privateContextBuffer.Add($"The developer cries for his {10 - _fingersLeft} lost fingers"); else _privateContextBuffer.Add($"The developer is greatful he still has {_fingersLeft} fingers even tho he had 10 once"); } } private void Die() { _isDead = true; Debug.Log($"{Name} ist verreckt."); } private bool IsGottfriedInRange() { float distanceToPlayer = Vector3.Distance(transform.position, GameManager.Instance.PlayerTransform.position); return distanceToPlayer <= _talkRange; } /// /// Returns the context stored in the contextBuffer seperated with ',' as a string /// /// public string GetPrivateContextAsString() { if (_privateContextBuffer.Count != 0) { string output = ""; foreach (string context in _privateContextBuffer) { output += context + ", "; } return output; } return string.Empty; } /// /// Starts talking when player is near to the Dev. /// public void TalkIfInRange() { if (IsGottfriedInRange() && !_isDead && !_isSleeping) { string context = GetPrivateContextAsString(); context += GameManager.Instance.GetContextAsString(); Talk(context); } } /// /// Dev starts talking. /// public void Talk(string context, int shortnessLevel = 0, bool priority = false) { if (priority) { _audioSource.Stop(); _audioSource.clip = null; } if (!_audioSource.isPlaying) { GetComponent().Generate(context, shortnessLevel); } } }