588 lines
20 KiB
C#
588 lines
20 KiB
C#
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;
|
|
public double UrinationUrgeFromCoffee;
|
|
public double UrinationUrgeFromMate;
|
|
public double UrinationUrgeFromPizza;
|
|
|
|
|
|
public DeveloperStats(double baseEfficiency, int fingers, double caffeineDrainFactor, double hungerDrainFactor, double urinationDrainFactor, double happinessDrainFactor, double urinationUrgeFromCoffee, double urinationUrgeFromMate, double urinationUrgeFromPizza)
|
|
{
|
|
BaseEfficiency = baseEfficiency;
|
|
Fingers = fingers;
|
|
CaffeineDrainFactor = caffeineDrainFactor;
|
|
HungerDrainFactor = hungerDrainFactor;
|
|
UrinationDrainFactor = urinationDrainFactor;
|
|
HappinessDrainFactor = happinessDrainFactor;
|
|
UrinationUrgeFromCoffee = urinationUrgeFromCoffee;
|
|
UrinationUrgeFromMate = urinationUrgeFromMate;
|
|
UrinationUrgeFromPizza = urinationUrgeFromPizza;
|
|
}
|
|
|
|
public static readonly DeveloperStats Default = new DeveloperStats(1.0, 10, 1, 1, 1, 1, 0.2, 0.2, 0.2);
|
|
}
|
|
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Gibt die Grunddaten des Entwicklers zurück.
|
|
/// </summary>
|
|
public DeveloperStats BaseStats => _baseStats;
|
|
|
|
/// <summary>
|
|
/// Gibt die Anzahl der Finger zurück.
|
|
/// </summary>
|
|
public int FingersLeft => _fingersLeft;
|
|
|
|
/// <summary>
|
|
/// Gibt die aktuelle Effizienz des Entwicklers in Prozent zurück.
|
|
/// </summary>
|
|
public double CurrentEfficiency => _currentEfficiency;
|
|
|
|
public double CurrentHappiness => _happiness;
|
|
public double CurrentUrgeToUrinate => _urgeToUrinateLevel;
|
|
public double CurrentHunger => _hungerLevel;
|
|
public double CurrentCaffeination => _caffeineLevel;
|
|
|
|
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<GameObject> _needList = new List<GameObject>();
|
|
private bool _isDead = false;
|
|
|
|
[SerializeField] private NPCAnimationController _npcAnimation;
|
|
|
|
/// <summary>
|
|
/// indicates wether the Dev is dead or not
|
|
/// </summary>
|
|
public bool IsDead => _isDead;
|
|
|
|
[SerializeField]
|
|
private int _maxPrivateContextBufferSize = 2;
|
|
[SerializeField]
|
|
private CircularBuffer<string> _privateContextBuffer;
|
|
|
|
/// <summary>
|
|
/// Returns the private Context Buffer
|
|
/// </summary>
|
|
public CircularBuffer<string> 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;
|
|
|
|
public bool IsOvercaffeinated => _isOvercaffeinated;
|
|
public bool IsHyperactive => _isHyperactive;
|
|
|
|
void Start()
|
|
{
|
|
_developerNeeds = gameObject.GetComponent<DeveloperNeeds>();
|
|
_audioSource = GetComponent<AudioSource>();
|
|
_talkTimer = Random.Range(5.0f, 15.0f);
|
|
_fingersLeft = _baseStats.Fingers;
|
|
_privateContextBuffer = new CircularBuffer<string>(_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;
|
|
}
|
|
|
|
if (drinkType == WantedConsumable.Coffee)
|
|
_urgeToUrinateLevel -= _baseStats.UrinationUrgeFromCoffee;
|
|
else if (drinkType == WantedConsumable.Mate)
|
|
_urgeToUrinateLevel -= _baseStats.UrinationUrgeFromMate;
|
|
|
|
_wantedDrink = WantedConsumable.None;
|
|
|
|
_npcAnimation.DrinkCoffee();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
_urgeToUrinateLevel -= _baseStats.UrinationUrgeFromPizza;
|
|
|
|
_wantedFood = WantedConsumable.None;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Der Entwickler Pinkelt die angegebene Menge aus.
|
|
/// </summary>
|
|
/// <param name="peeAmount">Die Menge die ausgepinkelt wird.</param>
|
|
/// <param name="onToilet">Wenn true, dann pinkelt der Entwickler auf einer Toilette. Wenn false, dann pinkelt er auf den Boden (was schlecht ist)</param>
|
|
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.HappinessDrainFactor;
|
|
|
|
_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;
|
|
|
|
bool wasOvercaffeinated = _isOvercaffeinated;
|
|
_isOvercaffeinated = _caffeineLevel > 1.5;
|
|
|
|
if (!wasOvercaffeinated && _isOvercaffeinated)
|
|
{
|
|
_npcAnimation.CaffeinOverdose();
|
|
}
|
|
|
|
_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);
|
|
|
|
_npcAnimation.GoToToilet();
|
|
}
|
|
|
|
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<SpinningSpinner>().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<Text2Speech>().speakingSpeed = 1.3f;
|
|
Talk("The Developer is surprisingly productive right now and feels an energyboost duo to caffeine", 0, true);
|
|
_hasTalkedWhileHyperactive = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_hasTalkedWhileHyperactive = false;
|
|
}
|
|
|
|
if (_isOvercaffeinated)
|
|
{
|
|
if (!_hasTalkedWhileOvercaffeinated)
|
|
{
|
|
GetComponent<Text2Speech>().speakingSpeed = 1.4f;
|
|
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;
|
|
}
|
|
|
|
if (!_isHyperactive && !_isOvercaffeinated)
|
|
GetComponent<Text2Speech>().speakingSpeed = 1.1f;
|
|
|
|
// 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 > GameManager.Instance.NeedNotificationThreshold)
|
|
return 1.0;
|
|
|
|
// https://easings.net/#easeOutExpo
|
|
return 1.0; //_urgeToUrinateLevel / GameManager.Instance.NeedNotificationThreshold;
|
|
|
|
//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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Der Entwickler wird verletzt und der Idiot bricht sich ausgerechnet einen Finger...
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the context stored in the contextBuffer seperated with ',' as a string
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public string GetPrivateContextAsString()
|
|
{
|
|
if (_privateContextBuffer.Count != 0)
|
|
{
|
|
string output = "";
|
|
foreach (string context in _privateContextBuffer)
|
|
{
|
|
output += context + ", ";
|
|
}
|
|
return output;
|
|
}
|
|
return string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts talking when player is near to the Dev.
|
|
/// </summary>
|
|
public void TalkIfInRange()
|
|
{
|
|
if (IsGottfriedInRange() && !_isDead && !_isSleeping && (_needList.Count == 0))
|
|
{
|
|
string context = GetPrivateContextAsString();
|
|
context += GameManager.Instance.GetContextAsString();
|
|
Talk(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dev starts talking.
|
|
/// </summary>
|
|
public void Talk(string context, int shortnessLevel = 0, bool priority = false)
|
|
{
|
|
if (priority)
|
|
{
|
|
_audioSource.Stop();
|
|
_audioSource.clip = null;
|
|
}
|
|
if (!_audioSource.isPlaying)
|
|
{
|
|
GetComponent<Text2Speech>().Generate(context, shortnessLevel);
|
|
}
|
|
}
|
|
|
|
public void PlayStepSound()
|
|
{
|
|
_audioSource.PlayOneShot(GameManager.Instance.Player.Data.StepSounds.GetRandomElement());
|
|
}
|
|
}
|