|
|
|
using UnityEngine;
|
|
|
|
using System;
|
|
|
|
using System.Collections;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
|
|
namespace Fungus
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* A rectangular screen area for rendering story text.
|
|
|
|
* Rooms may contain any number of Pages.
|
|
|
|
* If a Page is a child of a View, then transitioning to that View will automatically activate the Page.
|
|
|
|
*/
|
|
|
|
[ExecuteInEditMode]
|
|
|
|
public class Page : MonoBehaviour
|
|
|
|
{
|
|
|
|
/// Rectangular bounds used to display page text
|
|
|
|
public Bounds pageBounds = new Bounds(Vector3.zero, new Vector3(0.25f, 0.25f, 0f));
|
|
|
|
|
|
|
|
/// Page position within bounds when display height is less than bounds height
|
|
|
|
public enum VerticalAlign
|
|
|
|
{
|
|
|
|
Top,
|
|
|
|
Middle,
|
|
|
|
Bottom
|
|
|
|
}
|
|
|
|
|
|
|
|
public VerticalAlign verticalAlign = VerticalAlign.Middle;
|
|
|
|
|
|
|
|
string headerText = "";
|
|
|
|
string footerText = "";
|
|
|
|
|
|
|
|
string displayedStoryText = "";
|
|
|
|
string originalStoryText = "";
|
|
|
|
|
|
|
|
Action deferredAction;
|
|
|
|
Action continueAction;
|
|
|
|
|
|
|
|
public enum Mode
|
|
|
|
{
|
|
|
|
Idle,
|
|
|
|
Say,
|
|
|
|
Choose
|
|
|
|
};
|
|
|
|
|
|
|
|
[HideInInspector]
|
|
|
|
public Mode mode = Mode.Idle;
|
|
|
|
|
|
|
|
class Option
|
|
|
|
{
|
|
|
|
public string optionText;
|
|
|
|
public Action optionAction;
|
|
|
|
|
|
|
|
public Option(string _optionText, Action _optionAction)
|
|
|
|
{
|
|
|
|
optionText = _optionText;
|
|
|
|
optionAction = _optionAction;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
List<Option> options = new List<Option>();
|
|
|
|
|
|
|
|
float quickContinueTimer;
|
|
|
|
|
|
|
|
public virtual void Update()
|
|
|
|
{
|
|
|
|
if (quickContinueTimer > 0)
|
|
|
|
{
|
|
|
|
quickContinueTimer -= Time.deltaTime;
|
|
|
|
quickContinueTimer = Mathf.Max(quickContinueTimer, 0f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetHeader(string _headerText)
|
|
|
|
{
|
|
|
|
headerText = _headerText;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetFooter(string _footerText)
|
|
|
|
{
|
|
|
|
footerText = _footerText;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Say(string sayText, Action sayAction)
|
|
|
|
{
|
|
|
|
mode = Mode.Say;
|
|
|
|
StringTable stringTable = Game.GetInstance().stringTable;
|
|
|
|
string subbedText = stringTable.SubstituteStrings(sayText);
|
|
|
|
continueAction = sayAction;
|
|
|
|
WriteStory(subbedText);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void AddOption(string optionText, Action optionAction)
|
|
|
|
{
|
|
|
|
StringTable stringTable = Game.GetInstance().stringTable;
|
|
|
|
string subbedText = stringTable.FormatLinkText(stringTable.SubstituteStrings(optionText));
|
|
|
|
options.Add(new Option(subbedText, optionAction));
|
|
|
|
}
|
|
|
|
|
|
|
|
public void Choose(string _chooseText)
|
|
|
|
{
|
|
|
|
mode = Mode.Choose;
|
|
|
|
StringTable stringTable = Game.GetInstance().stringTable;
|
|
|
|
string subbedText = stringTable.SubstituteStrings(_chooseText);
|
|
|
|
WriteStory(subbedText);
|
|
|
|
}
|
|
|
|
|
|
|
|
void WriteStory(string storyText)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
if (pageStyle == null)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Disable quick continue for a short period to prevent accidental taps
|
|
|
|
quickContinueTimer = 0.8f;
|
|
|
|
|
|
|
|
originalStoryText = storyText;
|
|
|
|
|
|
|
|
// Hack to avoid displaying partial color tag text
|
|
|
|
if (storyText.Contains("<"))
|
|
|
|
{
|
|
|
|
displayedStoryText = storyText;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Use a coroutine to write the story text out over time
|
|
|
|
StartCoroutine(WriteStoryInternal());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Coroutine to write story text out over a period of time
|
|
|
|
IEnumerator WriteStoryInternal()
|
|
|
|
{
|
|
|
|
int charactersPerSecond = Game.GetInstance().charactersPerSecond;
|
|
|
|
|
|
|
|
// Zero CPS means write instantly
|
|
|
|
if (charactersPerSecond == 0)
|
|
|
|
{
|
|
|
|
displayedStoryText = originalStoryText;
|
|
|
|
yield break;
|
|
|
|
}
|
|
|
|
|
|
|
|
displayedStoryText = "";
|
|
|
|
|
|
|
|
// Make one character visible at a time
|
|
|
|
float writeDelay = (1f / (float)charactersPerSecond);
|
|
|
|
float timeAccumulator = 0f;
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
timeAccumulator += Time.deltaTime;
|
|
|
|
|
|
|
|
while (timeAccumulator > writeDelay)
|
|
|
|
{
|
|
|
|
i++;
|
|
|
|
timeAccumulator -= writeDelay;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (i >= originalStoryText.Length)
|
|
|
|
{
|
|
|
|
displayedStoryText = originalStoryText;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
string left = originalStoryText.Substring(0, i + 1);
|
|
|
|
string right = originalStoryText.Substring(i + 1);
|
|
|
|
|
|
|
|
displayedStoryText = left;
|
|
|
|
displayedStoryText += "<color=#FFFFFF00>";
|
|
|
|
displayedStoryText += right;
|
|
|
|
displayedStoryText += "</color>";
|
|
|
|
}
|
|
|
|
|
|
|
|
yield return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public virtual void OnGUI()
|
|
|
|
{
|
|
|
|
if (mode == Mode.Idle)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
if (pageStyle == null)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
GUIStyle boxStyle = pageStyle.boxStyle;
|
|
|
|
GUIStyle headerStyle = pageStyle.GetScaledHeaderStyle();
|
|
|
|
GUIStyle footerStyle = pageStyle.GetScaledFooterStyle();
|
|
|
|
GUIStyle sayStyle = pageStyle.GetScaledSayStyle();
|
|
|
|
GUIStyle optionStyle = pageStyle.GetScaledOptionStyle();
|
|
|
|
GUIStyle optionAlternateStyle = pageStyle.GetScaledOptionAlternateStyle();
|
|
|
|
GUIStyle continueStyle = pageStyle.GetScaledContinueStyle();
|
|
|
|
|
|
|
|
Rect pageRect = GetScreenRect();
|
|
|
|
Rect outerRect = FitRectToScreen(pageRect);
|
|
|
|
Rect innerRect = CalcInnerRect(outerRect);
|
|
|
|
|
|
|
|
// Calculate height of each section
|
|
|
|
float headerHeight = CalcHeaderHeight(innerRect.width);
|
|
|
|
float footerHeight = CalcFooterHeight(innerRect.width);
|
|
|
|
float storyHeight = CalcStoryHeight(innerRect.width);
|
|
|
|
float optionsHeight = CalcOptionsHeight(innerRect.width);
|
|
|
|
float contentHeight = headerHeight + footerHeight + storyHeight + optionsHeight;
|
|
|
|
|
|
|
|
// Adjust outer rect position based on alignment settings
|
|
|
|
switch (verticalAlign)
|
|
|
|
{
|
|
|
|
case VerticalAlign.Top:
|
|
|
|
outerRect.height = contentHeight + (boxStyle.padding.top + boxStyle.padding.bottom);
|
|
|
|
outerRect.y = pageRect.yMin;
|
|
|
|
break;
|
|
|
|
case VerticalAlign.Middle:
|
|
|
|
outerRect.height = contentHeight + (boxStyle.padding.top + boxStyle.padding.bottom);
|
|
|
|
outerRect.y = pageRect.center.y - outerRect.height / 2;
|
|
|
|
break;
|
|
|
|
case VerticalAlign.Bottom:
|
|
|
|
outerRect.height = contentHeight + (boxStyle.padding.top + boxStyle.padding.bottom);
|
|
|
|
outerRect.y = pageRect.yMax - outerRect.height;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Force outer rect to always be on-screen
|
|
|
|
// If the rect is bigger than the screen, then the top-left corner will always be visible
|
|
|
|
outerRect = FitRectToScreen(outerRect);
|
|
|
|
|
|
|
|
innerRect = CalcInnerRect(outerRect);
|
|
|
|
|
|
|
|
// Draw box
|
|
|
|
Rect boxRect = outerRect;
|
|
|
|
boxRect.height = contentHeight + (boxStyle.padding.top + boxStyle.padding.bottom);
|
|
|
|
GUI.Box(boxRect, "", boxStyle);
|
|
|
|
|
|
|
|
// Draw header label
|
|
|
|
Rect headerRect = innerRect;
|
|
|
|
headerRect.height = headerHeight;
|
|
|
|
if (headerHeight > 0)
|
|
|
|
{
|
|
|
|
GUI.Label(headerRect, headerText, headerStyle);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw say label
|
|
|
|
Rect storyRect = innerRect;
|
|
|
|
storyRect.y += headerHeight;
|
|
|
|
storyRect.height = storyHeight;
|
|
|
|
GUI.Label(storyRect, displayedStoryText, sayStyle);
|
|
|
|
|
|
|
|
// Draw footer label
|
|
|
|
Rect footerRect = innerRect;
|
|
|
|
footerRect.y += storyHeight;
|
|
|
|
footerRect.height = footerHeight;
|
|
|
|
if (footerHeight > 0)
|
|
|
|
{
|
|
|
|
GUI.Label(footerRect, footerText, footerStyle);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool finishedWriting = (displayedStoryText.Length == originalStoryText.Length);
|
|
|
|
if (!finishedWriting)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mode == Mode.Say)
|
|
|
|
{
|
|
|
|
Rect continueRect = CalcContinueRect(outerRect);
|
|
|
|
GUI.Button(continueRect, new GUIContent(Game.GetInstance().continueText), continueStyle);
|
|
|
|
|
|
|
|
// Player can continue by clicking anywhere
|
|
|
|
if (quickContinueTimer == 0 &&
|
|
|
|
(Input.GetMouseButtonUp(0) || Input.anyKeyDown) &&
|
|
|
|
continueAction != null)
|
|
|
|
{
|
|
|
|
deferredAction = continueAction;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (mode == Mode.Choose)
|
|
|
|
{
|
|
|
|
// Draw option buttons
|
|
|
|
Rect buttonRect = innerRect;
|
|
|
|
buttonRect.y += headerHeight + storyHeight;
|
|
|
|
bool alternateRow = false;
|
|
|
|
foreach (Option option in options)
|
|
|
|
{
|
|
|
|
GUIContent buttonContent = new GUIContent(option.optionText);
|
|
|
|
buttonRect.height = optionStyle.CalcHeight(buttonContent, innerRect.width);
|
|
|
|
|
|
|
|
// Select style for odd/even colored rows
|
|
|
|
GUIStyle style;
|
|
|
|
if (alternateRow)
|
|
|
|
{
|
|
|
|
style = optionAlternateStyle;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
style = optionStyle;
|
|
|
|
}
|
|
|
|
alternateRow = !alternateRow;
|
|
|
|
|
|
|
|
if (GUI.Button(buttonRect, buttonContent, style))
|
|
|
|
{
|
|
|
|
if (option.optionAction != null)
|
|
|
|
{
|
|
|
|
// We can't execute the option action yet because OnGUI
|
|
|
|
// may be called multiple times during a frame, and it's
|
|
|
|
// not permitted to modify GUI elements within a frame.
|
|
|
|
// We defer executing the action until OnGUI has completed.
|
|
|
|
deferredAction = option.optionAction;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
buttonRect.y += buttonRect.height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Event.current.type == EventType.Repaint)
|
|
|
|
{
|
|
|
|
if (deferredAction != null)
|
|
|
|
{
|
|
|
|
Game.GetInstance().PlayButtonClick();
|
|
|
|
|
|
|
|
Action tempAction = deferredAction;
|
|
|
|
|
|
|
|
displayedStoryText = "";
|
|
|
|
originalStoryText = "";
|
|
|
|
deferredAction = null;
|
|
|
|
|
|
|
|
if (mode == Mode.Choose)
|
|
|
|
{
|
|
|
|
options.Clear();
|
|
|
|
|
|
|
|
// Reset to idle, but calling action may set this again
|
|
|
|
mode = Mode.Idle;
|
|
|
|
|
|
|
|
CommandQueue commandQueue = Game.GetInstance().commandQueue;
|
|
|
|
commandQueue.CallCommandMethod(tempAction);
|
|
|
|
}
|
|
|
|
else if (mode == Mode.Say )
|
|
|
|
{
|
|
|
|
// Reset to idle, but calling action may set this again
|
|
|
|
mode = Mode.Idle;
|
|
|
|
|
|
|
|
// Execute next command
|
|
|
|
tempAction();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
float CalcHeaderHeight(float boxWidth)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
|
|
|
|
if (pageStyle == null ||
|
|
|
|
mode == Mode.Idle ||
|
|
|
|
headerText.Length == 0)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
GUIStyle headerStyle = pageStyle.GetScaledHeaderStyle();
|
|
|
|
|
|
|
|
GUIContent headerContent = new GUIContent(headerText);
|
|
|
|
return headerStyle.CalcHeight(headerContent, boxWidth);
|
|
|
|
}
|
|
|
|
|
|
|
|
float CalcFooterHeight(float boxWidth)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
|
|
|
|
if (pageStyle == null ||
|
|
|
|
mode == Mode.Idle ||
|
|
|
|
footerText.Length == 0)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
GUIStyle footerStyle = pageStyle.GetScaledFooterStyle();
|
|
|
|
|
|
|
|
GUIContent headerContent = new GUIContent(headerText);
|
|
|
|
return footerStyle.CalcHeight(headerContent, boxWidth);
|
|
|
|
}
|
|
|
|
|
|
|
|
float CalcStoryHeight(float boxWidth)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
GUIStyle sayStyle = pageStyle.GetScaledSayStyle();
|
|
|
|
|
|
|
|
if (pageStyle == null ||
|
|
|
|
mode == Mode.Idle ||
|
|
|
|
originalStoryText.Length == 0)
|
|
|
|
{
|
|
|
|
// Allow a space for story even if there's no text
|
|
|
|
return sayStyle.lineHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
GUIContent storyContent = new GUIContent(originalStoryText + "\n");
|
|
|
|
return sayStyle.CalcHeight(storyContent, boxWidth);
|
|
|
|
}
|
|
|
|
|
|
|
|
float CalcOptionsHeight(float boxWidth)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
|
|
|
|
if (pageStyle == null ||
|
|
|
|
mode == Mode.Idle ||
|
|
|
|
options.Count == 0)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This assumes that the alternate option style is the same height as the regular style
|
|
|
|
GUIStyle optionStyle = pageStyle.GetScaledOptionStyle();
|
|
|
|
|
|
|
|
float totalHeight = 0;
|
|
|
|
foreach (Option option in options)
|
|
|
|
{
|
|
|
|
GUIContent optionContent = new GUIContent(option.optionText);
|
|
|
|
float optionHeight = optionStyle.CalcHeight(optionContent, boxWidth);
|
|
|
|
totalHeight += optionHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add space at bottom
|
|
|
|
GUIStyle sayStyle = pageStyle.GetScaledSayStyle();
|
|
|
|
totalHeight += sayStyle.lineHeight;
|
|
|
|
|
|
|
|
return totalHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Force rect to always be on-screen
|
|
|
|
Rect FitRectToScreen(Rect rect)
|
|
|
|
{
|
|
|
|
Rect fittedRect = new Rect();
|
|
|
|
fittedRect.width = Mathf.Min(rect.width, Screen.width);
|
|
|
|
fittedRect.height = Mathf.Min(rect.height, Screen.height);
|
|
|
|
fittedRect.x = Mathf.Min(rect.x, Screen.width - rect.width);
|
|
|
|
fittedRect.y = Mathf.Min(rect.y, Screen.height - rect.height);
|
|
|
|
fittedRect.x = Mathf.Max(0, fittedRect.x);
|
|
|
|
fittedRect.y = Mathf.Max(0, fittedRect.y);
|
|
|
|
|
|
|
|
return fittedRect;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns smaller internal box rect with padding style applied
|
|
|
|
Rect CalcInnerRect(Rect outerRect)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
|
|
|
|
if (pageStyle == null)
|
|
|
|
{
|
|
|
|
return new Rect();
|
|
|
|
}
|
|
|
|
|
|
|
|
GUIStyle boxStyle = pageStyle.boxStyle;
|
|
|
|
|
|
|
|
Rect innerRect = new Rect(outerRect.x + boxStyle.padding.left,
|
|
|
|
outerRect.y + boxStyle.padding.top,
|
|
|
|
outerRect.width - (boxStyle.padding.left + boxStyle.padding.right),
|
|
|
|
outerRect.height - (boxStyle.padding.top + boxStyle.padding.bottom));
|
|
|
|
|
|
|
|
return innerRect;
|
|
|
|
}
|
|
|
|
|
|
|
|
Rect CalcContinueRect(Rect outerRect)
|
|
|
|
{
|
|
|
|
PageStyle pageStyle = Game.GetInstance().activePageStyle;
|
|
|
|
|
|
|
|
if (pageStyle == null)
|
|
|
|
{
|
|
|
|
return new Rect();
|
|
|
|
}
|
|
|
|
|
|
|
|
GUIStyle continueStyle = pageStyle.GetScaledContinueStyle();
|
|
|
|
|
|
|
|
GUIContent content = new GUIContent(Game.GetInstance().continueText);
|
|
|
|
float width = continueStyle.CalcSize(content).x;
|
|
|
|
float height = continueStyle.lineHeight;
|
|
|
|
|
|
|
|
float x = outerRect.xMin + (outerRect.width) - (width) - pageStyle.boxStyle.padding.right;
|
|
|
|
float y = outerRect.yMax - height / 2f;
|
|
|
|
|
|
|
|
return new Rect(x, y, width, height);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the page rect in screen space coords
|
|
|
|
*/
|
|
|
|
public Rect GetScreenRect()
|
|
|
|
{
|
|
|
|
// Y decreases up the screen in GUI space, so top left is rect origin
|
|
|
|
|
|
|
|
Vector3 topLeft = transform.position + pageBounds.center;
|
|
|
|
topLeft.x -= pageBounds.extents.x;
|
|
|
|
topLeft.y += pageBounds.extents.y;
|
|
|
|
|
|
|
|
Vector3 bottomRight = transform.position + pageBounds.center;
|
|
|
|
bottomRight.x += pageBounds.extents.x;
|
|
|
|
bottomRight.y -= pageBounds.extents.y;
|
|
|
|
|
|
|
|
Camera mainCamera = GameObject.FindGameObjectWithTag("MainCamera").camera;
|
|
|
|
Vector2 tl = mainCamera.WorldToScreenPoint(topLeft);
|
|
|
|
Vector2 br = mainCamera.WorldToScreenPoint(bottomRight);
|
|
|
|
|
|
|
|
Rect pageRect = new Rect(tl.x, Screen.height - tl.y, br.x - tl.x, tl.y - br.y);
|
|
|
|
|
|
|
|
return FitRectToScreen(pageRect);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|