LADXHD/InGame/Overlay/TextboxOverlay.cs
2023-12-14 17:21:22 -05:00

685 lines
26 KiB
C#

using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ProjectZ.Base;
using ProjectZ.Base.UI;
using ProjectZ.InGame.Controls;
using ProjectZ.InGame.GameObjects.Base;
using ProjectZ.InGame.Map;
using ProjectZ.InGame.SaveLoad;
using ProjectZ.InGame.Things;
namespace ProjectZ.InGame.Overlay
{
public class TextboxOverlay
{
struct ChoiceButton
{
public float Percentage;
public float SelectionPercentage;
}
public Rectangle DialogBoxTextBox;
public bool IsOpen;
public bool OwlMode;
public bool UpdateObjects;
public float TransitionState => _currentOpacity;
private const int ScrollSpeed = 20;
private readonly Animator _animator;
private readonly UiRectangle _textboxBackground;
//private readonly UiRectangle _textboxBackgroundSide;
private readonly UiRectangle[] _textboxBackgroundChoice = new UiRectangle[4];
private readonly ChoiceButton[] _textboxChoice = new ChoiceButton[4];
private Rectangle _dialogBoxRectangle;
private Point _letterSize;
private string _strFullText;
private string _strDialog;
private string _choiceKey;
private string[] _choices;
private float _textScrollCounter;
private float _textMult;
private float _textboxOffsetY;
private float _currentOpacity;
private const float TransitionSpeed = 0.15f;
private int _textOffsetY;
private float _selectionCounter;
private const int SelectionTime = 250;
private int _currentDialogCount;
private int _paddingLeft = 5;
private int _paddingRight = 12;
private int _paddingV = 5;
private int _textboxMargin = 16;
private int _uiScale = 4;
private int _dialogBoxWidth = 200;
private int _dialogBoxHeight;
private int _currentState;
private int _currentLine;
private int _currentLineAddition;
private const int MaxCharacters = 26;
private const int MaxLines = 3;
private float _choicePercentage;
private int _currentChoiceSelection;
private int _choiceWidth;
private bool _running = true;
private bool _end;
private bool _isChoice;
private bool _openDialog = false;
private bool _boxEnd;
private string _scrollText;
private float _scrollCounter;
private const int ScrollTime = 125;
private bool _textScrolling;
public TextboxOverlay()
{
_animator = AnimatorSaveLoad.LoadAnimator("dialog_arrow");
// @HACK
_textboxBackground = new UiRectangle(Rectangle.Empty, "textboxblur", Values.ScreenNameGame, Color.Transparent, Color.Transparent, null) { Radius = Values.UiTextboxRadius };
Game1.EditorUi.AddElement(_textboxBackground);
//_textboxBackgroundSide = new UiRectangle(Rectangle.Empty, "textboxblur", Values.ScreenNameGame, Color.Transparent, Color.Transparent, null) { Radius = Values.UiTextboxRadius };
//Game1.EditorUi.AddElement(_textboxBackgroundSide);
for (var i = 0; i < _textboxBackgroundChoice.Length; i++)
{
_textboxBackgroundChoice[i] = new UiRectangle(Rectangle.Empty, "", Values.ScreenNameGame, Color.Transparent, Color.Transparent, null) { Radius = Values.UiTextboxRadius };
Game1.EditorUi.AddElement(_textboxBackgroundChoice[i]);
}
}
public void Init()
{
IsOpen = false;
_currentOpacity = 0;
UpdateTextBoxState();
}
public void Update()
{
if (MapManager.ObjLink.IsTransitioning)
return;
// update the opacity of the textbox background
if (IsOpen && _currentOpacity < 1)
_currentOpacity += TransitionSpeed * Game1.TimeMultiplier;
else if (!IsOpen && _currentOpacity > 0)
_currentOpacity -= TransitionSpeed * Game1.TimeMultiplier;
_currentOpacity = MathHelper.Clamp(_currentOpacity, 0, 1);
if (_currentOpacity <= 0 && _openDialog)
{
_openDialog = false;
Game1.GameManager.SaveManager.SetString("dialogOpen", "0");
}
if (_currentOpacity >= 1)
UpdateDialogBox();
_textboxOffsetY = (float)Math.Sin((1 - _currentOpacity) * Math.PI / 2) * 3 * _uiScale;
UpdateTextBoxState();
if (_isChoice && !_running && _end)
{
_choicePercentage = AnimationHelper.MoveToTarget(_choicePercentage, 1, 0.075f * Game1.TimeMultiplier);
_choiceWidth = 0;
for (var i = 0; i < _choices.Length; i++)
{
var width = ((int)Resources.GameFont.MeasureString("" + _choices[i] + "").X + 8) * _uiScale;
if (_choiceWidth < width)
_choiceWidth = width;
}
for (var i = 0; i < _choices.Length; i++)
{
if (_choicePercentage >= i * 0.25f)
_textboxChoice[i].Percentage = AnimationHelper.MoveToTarget(_textboxChoice[i].Percentage, 1, TransitionSpeed * Game1.TimeMultiplier);
_textboxChoice[i].SelectionPercentage = AnimationHelper.MoveToTarget(_textboxChoice[i].SelectionPercentage, _currentChoiceSelection == i ? 1 : 0, TransitionSpeed * Game1.TimeMultiplier);
var choicePositionY = _dialogBoxHeight + 1;
var padding = (int)(_textboxChoice[i].SelectionPercentage * _uiScale);
_textboxBackgroundChoice[i].BackgroundColor = Color.Lerp(Values.TextboxBackgroundColor, Values.TextboxFontColor, _textboxChoice[i].SelectionPercentage) * 0.85f * _currentOpacity * _textboxChoice[i].Percentage;
_textboxBackgroundChoice[i].BlurColor = Values.TextboxBlurColor * _currentOpacity * _textboxChoice[i].Percentage;
_textboxBackgroundChoice[i].Rectangle = new Rectangle(
_dialogBoxRectangle.X + _dialogBoxRectangle.Width - _choiceWidth * (_choices.Length - i) - (3 * _uiScale) * (_choices.Length - 1 - i) - padding - _uiScale,
_dialogBoxRectangle.Y + choicePositionY * _uiScale + (int)_textboxOffsetY - padding + (int)(Math.Sin((1 - _textboxChoice[i].Percentage) * Math.PI / 2) * 4 * _uiScale),
_choiceWidth + 2 * padding, (Resources.GameFontHeight + 4) * _uiScale + 2 * padding);
}
}
UpdateGameState();
}
private void UpdateTextBoxState()
{
// set textbox fade in from the bottom
_textboxBackground.BackgroundColor = Values.TextboxBackgroundColor * _currentOpacity;
_textboxBackground.BlurColor = Values.TextboxBlurColor * _currentOpacity;
_textboxBackground.Rectangle.Y = _dialogBoxRectangle.Y + (int)_textboxOffsetY;
//_textboxBackgroundSide.BackgroundColor = Values.TextboxBackgroundSideColor * _currentOpacity;
//_textboxBackgroundSide.BlurColor = Values.TextboxBlurColor * _currentOpacity;
//_textboxBackgroundSide.Rectangle.Y = _dialogBoxRectangle.Y + (int)_textboxOffsetY;
}
private void UpdateGameState()
{
// this function gets called by StartDialogX and Freezes the game when a dialog is loaded for the map we are transitioning in; not updating the transition for one frame
if (MapManager.ObjLink.IsTransitioning)
return;
if (_openDialog && !UpdateObjects)
Game1.UpdateGame = false;
// needs to be called before the UpdateDialogBox method to ensure the player is frozen between a dialog box and a [freeze:x] coming after a [wait:dialogOpen:1]
if (_openDialog && UpdateObjects)
MapManager.ObjLink.FreezePlayer();
}
public void DrawTop(SpriteBatch spriteBatch)
{
if (_currentOpacity <= 0)
return;
var scrollOffset = 0f;
if (_textScrolling)
{
var scrollPercentage = _scrollCounter / ScrollTime;
scrollOffset = scrollPercentage * Resources.GameFontHeight * _uiScale;
spriteBatch.DrawString(Resources.GameFont, _scrollText,
new Vector2(DialogBoxTextBox.X, DialogBoxTextBox.Y + _textboxOffsetY - Resources.GameFontHeight * _uiScale + scrollOffset + _textOffsetY * _uiScale),
Values.TextboxFontColor * _currentOpacity * Math.Clamp(scrollPercentage * 2f - 1f, 0, 1), 0, Vector2.Zero, _uiScale, SpriteEffects.None, 0);
}
// draw the dialog box
spriteBatch.DrawString(Resources.GameFont, _strDialog,
new Vector2(DialogBoxTextBox.X, DialogBoxTextBox.Y + _textboxOffsetY + scrollOffset + _textOffsetY * _uiScale),
Values.TextboxFontColor * _currentOpacity, 0, Vector2.Zero, _uiScale, SpriteEffects.None, 0);
if (!_running && !_end)
{
_animator.DrawBasic(spriteBatch, new Vector2(_dialogBoxRectangle.Right - _uiScale * 2, _dialogBoxRectangle.Bottom - _uiScale * 2), Color.White, _uiScale);
}
// show the choices if the text is fully shown
if (_isChoice && !_running && _end)
{
for (var i = 0; i < _choices.Length; i++)
{
var textSize = Resources.GameFont.MeasureString(_choices[i]);
var color = Color.Lerp(Values.TextboxFontColor, Values.TextboxBackgroundColor, _textboxChoice[i].SelectionPercentage);
var posX = _textboxBackgroundChoice[i].Rectangle.X + _textboxBackgroundChoice[i].Rectangle.Width / 2 - (textSize.X * _uiScale) / 2;
var posY = _textboxBackgroundChoice[i].Rectangle.Y + _textboxBackgroundChoice[i].Rectangle.Height / 2 - (textSize.Y * _uiScale) / 2 + _uiScale;
spriteBatch.DrawString(Resources.GameFont, _choices[i],
new Vector2((int)posX, (int)posY), color * _currentOpacity * _textboxChoice[i].Percentage, 0, Vector2.Zero, _uiScale, SpriteEffects.None, 0);
}
}
}
public void UpdateDialogBox()
{
_animator.Update();
if (_isChoice && !_running && _end)
ChoiceUpdate();
if (_textScrolling)
{
_scrollCounter -= Game1.DeltaTime;
if (_scrollCounter <= 0)
{
_textScrolling = false;
_scrollCounter = 0;
}
return;
}
if (ControlHandler.ButtonPressed(CButtons.A))
{
// close the dialog box
if (_end)
{
OwlMode = false;
IsOpen = false;
InputHandler.ResetInputState();
// set the choice variable
if (_isChoice)
Game1.GameManager.SaveManager.SetString(_choiceKey, _currentChoiceSelection.ToString());
}
else
{
// start next box
if (!_running)
{
_textMult = 1;
_running = true;
// start text in new textbox
if (_boxEnd)
{
_currentLine = 0;
_currentState += _currentDialogCount + 1;
_currentDialogCount = 0;
_textScrollCounter = -150;
_strDialog = "";
_textOffsetY = CalculateTextOffsetY(_strFullText);
}
// continue scrolling the text up
else
{
_currentLineAddition = 0;
}
}
// jump to end
else
{
_textMult = 4;
}
}
}
// scroll text
if (_running)
_textScrollCounter += Game1.DeltaTime * _textMult;
var updated = false;
while (_running && _currentState + _currentDialogCount < _strFullText.Length && _textScrollCounter > ScrollSpeed)
{
updated = true;
_textScrollCounter -= ScrollSpeed;
NextLetter(false);
}
if (updated)
{
_strDialog = _strFullText.Substring(_currentState, _currentDialogCount);
}
if (_running && _currentState + _currentDialogCount >= _strFullText.Length)
{
_end = true;
_running = false;
Game1.GameManager.PlaySoundEffect("D360-21-15");
}
}
private void NextLetter(bool fastForward)
{
if (_strFullText[_currentState + _currentDialogCount] == '\f')
{
_animator.Stop();
_animator.Play("idle");
_boxEnd = true;
_running = false;
_currentLineAddition = MaxLines;
return;
}
// line break?
if (_strFullText[_currentState + _currentDialogCount] == '\n')
{
if (_currentLine + 1 < MaxLines)
_currentLine++;
else if (_currentLineAddition < MaxLines)
{
_currentLineAddition++;
_textScrolling = true;
_scrollCounter = ScrollTime;
var offset = _strFullText.IndexOf('\n', _currentState) - _currentState + 1;
_scrollText = _strFullText.Substring(_currentState, offset);
_strDialog = _strFullText.Substring(_currentState, _currentDialogCount);
_currentState += offset;
_currentDialogCount -= offset;
}
else
{
_animator.Stop();
_animator.Play("idle");
_boxEnd = false;
_running = false;
if (!fastForward)
Game1.GameManager.PlaySoundEffect("D360-21-15");
return;
}
}
if (!fastForward && !OwlMode && _running && _currentDialogCount % 6 == 0)
Game1.GameManager.PlaySoundEffect("D370-15-0F", true);
if (!fastForward && OwlMode && _running && _currentDialogCount % 28 == 0)
Game1.GameManager.PlaySoundEffect("D370-25-19", true);
_currentDialogCount++;
}
public void ChoiceUpdate()
{
var newSelection = _currentChoiceSelection;
var direction = ControlHandler.GetMoveVector2();
var dir = AnimationHelper.GetDirection(direction);
if (direction.Length() > Values.ControllerDeadzone && (dir == 0 || dir == 2))
{
_selectionCounter -= Game1.DeltaTime;
if (_selectionCounter <= 0)
{
_selectionCounter += SelectionTime;
if (dir == 0)
newSelection--;
else if (dir == 2)
newSelection++;
}
}
else
{
_selectionCounter = 0;
}
newSelection = MathHelper.Clamp(newSelection, 0, _choices.Length - 1);
if (_currentChoiceSelection != newSelection)
{
_currentChoiceSelection = newSelection;
Game1.GameManager.PlaySoundEffect("D360-10-0A");
}
}
public void ResolutionChange()
{
_uiScale = Game1.UiScale;
SetUpDialogBox();
}
public void SetUpDialogBox()
{
// only works if every letter has the same size
_letterSize = new Point((int)Resources.GameFont.MeasureString("A").X, (int)Resources.GameFont.MeasureString("A").Y);
_dialogBoxWidth = _letterSize.X * MaxCharacters + _paddingLeft + _paddingRight;
_dialogBoxHeight = _letterSize.Y * MaxLines + _paddingV * 2;
_dialogBoxRectangle = new Rectangle(
Game1.WindowWidth / 2 - _dialogBoxWidth * _uiScale / 2,
Game1.WindowHeight - (_uiScale * _dialogBoxHeight) - _uiScale * _textboxMargin, _dialogBoxWidth * _uiScale, _dialogBoxHeight * _uiScale);
DialogBoxTextBox = new Rectangle(
_dialogBoxRectangle.X + _paddingLeft * _uiScale,
_dialogBoxRectangle.Y + _paddingV * _uiScale,
_dialogBoxRectangle.Width - (_paddingLeft + _paddingRight) * _uiScale,
_dialogBoxRectangle.Height - (_paddingV * 2) * _uiScale);
_textboxBackground.Rectangle = _dialogBoxRectangle;
//_textboxBackgroundSide.Rectangle = new Rectangle(
// _dialogBoxRectangle.X - 2 * _uiScale,
// _dialogBoxRectangle.Y, 2 * _uiScale, _dialogBoxRectangle.Height);
}
public void StartDialog(string dialogText)
{
_openDialog = true;
Game1.GameManager.SaveManager.SetString("dialogOpen", "1");
_currentLine = 0;
_currentLineAddition = MaxLines;
_currentState = 0;
_currentDialogCount = 0;
_textMult = 1;
_strFullText = SetUpString(dialogText);
_textOffsetY = CalculateTextOffsetY(_strFullText);
_strDialog = "";
_end = false;
IsOpen = true;
_running = true;
_isChoice = false;
ResetChoice();
// needs to be called to make sure to freeze the game directly after the dialog was started
UpdateGameState();
}
public void StartChoice(string choiceKey, string choiceText, params string[] choices)
{
_openDialog = true;
Game1.GameManager.SaveManager.SetString("dialogOpen", "1");
_currentLine = 0;
_currentLineAddition = MaxLines;
_currentState = 0;
_currentDialogCount = 0;
_textMult = 1;
_choiceKey = choiceKey;
_strFullText = SetUpString(choiceText);
_textOffsetY = CalculateTextOffsetY(_strFullText);
_choices = choices;
_strDialog = "";
_end = false;
IsOpen = true;
_running = true;
_isChoice = true;
ResetChoice();
// needs to be called to make sure to freeze the game directly after the dialog was started
UpdateGameState();
}
private void ResetChoice()
{
_choicePercentage = 0;
for (var i = 0; i < _textboxChoice.Length; i++)
{
_textboxChoice[i].Percentage = 0;
_textboxChoice[i].SelectionPercentage = 0;
_textboxBackgroundChoice[i].BlurColor = Color.Transparent;
_textboxBackgroundChoice[i].BackgroundColor = Color.Transparent;
}
_currentChoiceSelection = 0;
_textboxChoice[0].SelectionPercentage = 1;
}
public string ReplaceKeys(string inputString)
{
string outputString = inputString;
int openIndex;
int closeIndex;
do
{
openIndex = inputString.IndexOf('[');
closeIndex = openIndex + 1;
if (openIndex != -1)
{
closeIndex = inputString.IndexOf(']', closeIndex + 1);
if (closeIndex != -1)
{
var stringKey = inputString.Substring(openIndex + 1, closeIndex - openIndex - 1);
var value = Game1.GameManager.SaveManager.GetString(stringKey);
if (value != null)
{
inputString = inputString.Remove(openIndex, closeIndex - openIndex + 1);
inputString = inputString.Insert(openIndex, value);
}
}
else
break;
}
} while (openIndex != -1);
return outputString;
}
public string SetUpString(string inputString)
{
// put in the players name
inputString = inputString.Replace("[NAME]", Game1.GameManager.SaveName);
inputString = ReplaceKeys(inputString);
return SetUpStringSplit(inputString);
}
public int CalculateTextOffsetY(string inputString)
{
// center text if \f is used
var fIndex = inputString.IndexOf('\f', _currentState);
if (fIndex > 0)
inputString = inputString.Substring(_currentState, -_currentState + fIndex);
else
inputString = inputString.Substring(_currentState);
var lineBreaks = inputString.Count(c => c == '\n');
if (lineBreaks < 2)
return ((MaxLines - lineBreaks - 1) * Resources.GameFontHeight) / 2;
else
return 0;
}
public string SetUpStringSplit(string inputString)
{
if (inputString == null)
return "Error";
inputString = inputString.Replace("\\n", "\n");
inputString = inputString.Replace("\\f", "\f");
var outString = "";
var currentState = 0;
var lines = 0;
while (currentState < inputString.Length)
{
lines++;
var subString = inputString.Substring(currentState, Math.Min(MaxCharacters, inputString.Length - currentState));
var indexN = subString.IndexOf('\n');
var indexF = subString.IndexOf('\f');
var indexC = subString.IndexOf("{");
indexN = indexN == -1 ? 999 : indexN;
indexF = indexF == -1 ? 999 : indexF;
indexC = indexC == -1 ? 999 : indexC;
// add a new line
if (indexC != 999 && indexC < indexN && indexC < indexF)
{
// finish the line
if (indexC != 0)
{
outString += subString.Substring(0, indexC) + "\n";
currentState += indexC;
continue;
}
var closeIndex = subString.IndexOf('}', indexC);
if (closeIndex != -1)
{
var strCenter = subString.Substring(indexC + 1, closeIndex - (indexC + 1));
for (var i = 0; i < (MaxCharacters - strCenter.Length) / 2; i++)
outString += " ";
outString += strCenter;
// new line when there is still text and no new textbox
if (currentState + closeIndex + 1 < inputString.Length &&
inputString[currentState + closeIndex + 1] != '\f')
outString += "\n";
currentState += closeIndex + 1;
}
else
{
// this should not happen
currentState += MaxCharacters;
outString += subString;
}
}
else if (indexN != 999 && indexN < indexC && indexN < indexF)
{
currentState = currentState + indexN + 1;
outString += subString.Substring(0, indexN + 1);
}
// add empty text
else if (indexF != 999)
{
currentState = currentState + indexF + 1;
outString += subString.Substring(0, indexF) + "\f";
}
// finished?
else if (inputString.Length - currentState <= MaxCharacters)
{
outString += subString;
break;
}
else
{
// find a " " to add a line break
var splitString = false;
for (var i = 0; i < MaxCharacters; i++)
{
if (inputString[currentState + MaxCharacters - i] == ' ')
{
splitString = true;
outString += inputString.Substring(currentState, MaxCharacters - i) + "\n";
currentState = currentState + MaxCharacters - i + 1;
break;
}
}
if (!splitString)
{
outString += inputString.Substring(currentState, MaxCharacters) + "\n";
currentState = currentState + MaxCharacters;
}
}
}
return outString;
}
}
}