Browse Source

Page rendering improvements

- Replaced Title() command with Header() and Footer()
- Added auto hide timer for buttons
- Changed delayed text writing to use a more reliable method
master
chrisgregan 11 years ago
parent
commit
9a1943dfe6
  1. BIN
      Assets/Fungus/Prefabs/PageStyle1.prefab
  2. BIN
      Assets/Fungus/Prefabs/PageStyle2.prefab
  3. 24
      Assets/Fungus/Scripts/Button.cs
  4. 40
      Assets/Fungus/Scripts/Commands.cs
  5. 23
      Assets/Fungus/Scripts/Game.cs
  6. 32
      Assets/Fungus/Scripts/GameController.cs
  7. 179
      Assets/Fungus/Scripts/Page.cs
  8. 34
      Assets/Fungus/Scripts/PageStyle.cs
  9. BIN
      Assets/FungusExample/Scenes/Example.unity
  10. 8
      Assets/FungusExample/Scripts/PageRoom.cs

BIN
Assets/Fungus/Prefabs/PageStyle1.prefab

Binary file not shown.

BIN
Assets/Fungus/Prefabs/PageStyle2.prefab

Binary file not shown.

24
Assets/Fungus/Scripts/Button.cs

@ -1,4 +1,4 @@
using UnityEngine; using UnityEngine;
using System; using System;
using System.Collections; using System.Collections;
using Fungus; using Fungus;
@ -80,7 +80,7 @@ namespace Fungus
if (autoHide) if (autoHide)
{ {
if (showButton && if (showButton &&
Game.GetInstance().IsGameIdle()) Game.GetInstance().ShowAutoButtons())
{ {
targetAlpha = 1f; targetAlpha = 1f;
@ -104,18 +104,28 @@ namespace Fungus
UpdateTargetAlpha(); UpdateTargetAlpha();
SpriteRenderer spriteRenderer = renderer as SpriteRenderer; SpriteRenderer spriteRenderer = renderer as SpriteRenderer;
Color color = spriteRenderer.color;
float fadeSpeed = (1f / Game.GetInstance().buttonFadeDuration); float fadeSpeed = (1f / Game.GetInstance().buttonFadeDuration);
color.a = Mathf.MoveTowards(color.a, targetAlpha, Time.deltaTime * fadeSpeed);
spriteRenderer.color = color; float alpha = Mathf.MoveTowards(spriteRenderer.color.a, targetAlpha, Time.deltaTime * fadeSpeed);;
// Set alpha for this sprite and any child sprites
SpriteRenderer[] children = spriteRenderer.gameObject.GetComponentsInChildren<SpriteRenderer>();
foreach (SpriteRenderer child in children)
{
Color color = child.color;
color.a = alpha;
child.color = color;
}
} }
void OnMouseUpAsButton() void OnMouseUpAsButton()
{ {
SpriteRenderer spriteRenderer = renderer as SpriteRenderer; SpriteRenderer spriteRenderer = renderer as SpriteRenderer;
// Ignore button press if sprite is not fully visible // Ignore button press if sprite is not fully visible or
if (spriteRenderer.color.a != 1f) // if the game is not in an idle state
if (spriteRenderer.color.a != 1f ||
!Game.GetInstance().ShowAutoButtons())
{ {
return; return;
} }

40
Assets/Fungus/Scripts/Commands.cs

@ -184,13 +184,13 @@ namespace Fungus
} }
/** /**
* Sets the title text displayed at the top of the active page. * Sets the header text displayed at the top of the active page.
*/ */
public class TitleCommand : CommandQueue.Command public class HeaderCommand : CommandQueue.Command
{ {
string titleText; string titleText;
public TitleCommand(string _titleText) public HeaderCommand(string _titleText)
{ {
titleText = _titleText; titleText = _titleText;
} }
@ -204,7 +204,37 @@ namespace Fungus
} }
else else
{ {
page.SetTitle(titleText); page.SetHeader(titleText);
}
if (onComplete != null)
{
onComplete();
}
}
}
/**
* Sets the footer text displayed at the top of the active page.
*/
public class FooterCommand : CommandQueue.Command
{
string titleText;
public FooterCommand(string _titleText)
{
titleText = _titleText;
}
public override void Execute(CommandQueue commandQueue, Action onComplete)
{
Page page = Game.GetInstance().activePage;
if (page == null)
{
Debug.LogError("Active page must not be null");
}
else
{
page.SetFooter(titleText);
} }
if (onComplete != null) if (onComplete != null)
{ {
@ -326,7 +356,7 @@ namespace Fungus
// Fade out screen // Fade out screen
game.cameraController.Fade(0f, game.roomFadeDuration / 2f, delegate { game.cameraController.Fade(0f, game.roomFadeDuration / 2f, delegate {
game.activeRoom = room; game.activeRoom = room;
// Notify room script that the Room is being entered // Notify room script that the Room is being entered

23
Assets/Fungus/Scripts/Game.cs

@ -63,6 +63,10 @@ namespace Fungus
*/ */
public AudioClip buttonClickClip; public AudioClip buttonClickClip;
public float autoHideButtonDuration = 5f;
float autoHideButtonTimer;
/** /**
* Global dictionary of integer values for storing game state. * Global dictionary of integer values for storing game state.
*/ */
@ -134,6 +138,17 @@ namespace Fungus
} }
} }
public virtual void Update()
{
autoHideButtonTimer -= Time.deltaTime;
autoHideButtonTimer = Mathf.Max(autoHideButtonTimer, 0f);
if (Input.GetMouseButtonDown(0))
{
autoHideButtonTimer = autoHideButtonDuration;
}
}
/** /**
* Plays the button clicked sound effect * Plays the button clicked sound effect
*/ */
@ -147,10 +162,10 @@ namespace Fungus
} }
/** /**
* Returns true if the game is in an idle state. * Returns true if the game should display 'auto hide' buttons.
* The game is in and idle state if the active page is not currently displaying story text/options, and no Wait command is in progress * Buttons will be displayed if the active page is not currently displaying story text/options, and no Wait command is in progress.
*/ */
public bool IsGameIdle() public bool ShowAutoButtons()
{ {
if (waiting) if (waiting)
{ {
@ -160,7 +175,7 @@ namespace Fungus
if (activePage == null || if (activePage == null ||
activePage.mode == Page.Mode.Idle) activePage.mode == Page.Mode.Idle)
{ {
return true; return (autoHideButtonTimer > 0f);
} }
return false; return false;

32
Assets/Fungus/Scripts/GameController.cs

@ -159,15 +159,36 @@ namespace Fungus
} }
/** /**
* Sets the title text displayed at the top of the active Page. * Obsolete! Use Header() instead.
* The title text is only displayed when there is some story text or options to be shown.
* This method returns immediately but it queues an asynchronous command for later execution.
* @param titleText The text to display as the title of the Page.
*/ */
[System.Obsolete("use Header() instead")]
public static void Title(string titleText) public static void Title(string titleText)
{
Header(titleText);
}
/**
* Sets the header text displayed at the top of the active Page.
* The header text is only displayed when there is some story text or options to be shown.
* This method returns immediately but it queues an asynchronous command for later execution.
* @param footerText The text to display as the header of the Page.
*/
public static void Header(string headerText)
{
CommandQueue commandQueue = Game.GetInstance().commandQueue;
commandQueue.AddCommand(new Command.HeaderCommand(headerText));
}
/**
* Sets the footer text displayed at the top of the active Page.
* The footer text is only displayed when there is some story text or options to be shown.
* This method returns immediately but it queues an asynchronous command for later execution.
* @param footerText The text to display as the footer of the Page.
*/
public static void Footer(string footerText)
{ {
CommandQueue commandQueue = Game.GetInstance().commandQueue; CommandQueue commandQueue = Game.GetInstance().commandQueue;
commandQueue.AddCommand(new Command.TitleCommand(titleText)); commandQueue.AddCommand(new Command.FooterCommand(footerText));
} }
/** /**
@ -375,7 +396,6 @@ namespace Fungus
/** /**
* Makes a sprite stop behaving as a clickable button. * Makes a sprite stop behaving as a clickable button.
* Removes the Button component from the sprite object.
* This method returns immediately but it queues an asynchronous command for later execution. * This method returns immediately but it queues an asynchronous command for later execution.
* @param spriteRenderer The sprite to be made non-clickable * @param spriteRenderer The sprite to be made non-clickable
*/ */

179
Assets/Fungus/Scripts/Page.cs

@ -27,10 +27,11 @@ namespace Fungus
public VerticalAlign verticalAlign = VerticalAlign.Middle; public VerticalAlign verticalAlign = VerticalAlign.Middle;
string titleText = ""; string headerText = "";
string footerText = "";
string originalStoryText = "";
string displayedStoryText = ""; string displayedStoryText = "";
string originalStoryText = "";
Action deferredAction; Action deferredAction;
Action continueAction; Action continueAction;
@ -42,6 +43,7 @@ namespace Fungus
Choose Choose
}; };
[HideInInspector]
public Mode mode = Mode.Idle; public Mode mode = Mode.Idle;
class Option class Option
@ -69,9 +71,14 @@ namespace Fungus
} }
} }
public void SetTitle(string _titleText) public void SetHeader(string _headerText)
{ {
titleText = _titleText; headerText = _headerText;
}
public void SetFooter(string _footerText)
{
footerText = _footerText;
} }
public void Say(string sayText, Action sayAction) public void Say(string sayText, Action sayAction)
@ -106,23 +113,18 @@ namespace Fungus
return; return;
} }
GUIStyle sayStyle = pageStyle.GetScaledSayStyle();
// Disable quick continue for a short period to prevent accidental taps // Disable quick continue for a short period to prevent accidental taps
quickContinueTimer = 0.8f; quickContinueTimer = 0.8f;
originalStoryText = storyText;
// Hack to avoid displaying partial color tag text // Hack to avoid displaying partial color tag text
if (storyText.Contains("<")) if (storyText.Contains("<"))
{ {
originalStoryText = storyText;
displayedStoryText = storyText; displayedStoryText = storyText;
} }
else else
{ {
float textWidth = CalcInnerRect(GetScreenRect()).width;
originalStoryText = InsertLineBreaks(storyText, sayStyle, textWidth);
displayedStoryText = "";
// Use a coroutine to write the story text out over time // Use a coroutine to write the story text out over time
StartCoroutine(WriteStoryInternal()); StartCoroutine(WriteStoryInternal());
} }
@ -134,34 +136,47 @@ namespace Fungus
int charactersPerSecond = Game.GetInstance().charactersPerSecond; int charactersPerSecond = Game.GetInstance().charactersPerSecond;
// Zero CPS means write instantly // Zero CPS means write instantly
if (charactersPerSecond <= 0) if (charactersPerSecond == 0)
{ {
displayedStoryText = originalStoryText; displayedStoryText = originalStoryText;
yield break; yield break;
} }
displayedStoryText = ""; displayedStoryText = "";
float writeDelay = 1f / (float)charactersPerSecond;
// Make one character visible at a time
float writeDelay = (1f / (float)charactersPerSecond);
float timeAccumulator = 0f; float timeAccumulator = 0f;
int i = 0;
while (displayedStoryText.Length < originalStoryText.Length) while (true)
{ {
timeAccumulator += Time.deltaTime; timeAccumulator += Time.deltaTime;
while (timeAccumulator > 0f) while (timeAccumulator > writeDelay)
{ {
i++;
timeAccumulator -= writeDelay; timeAccumulator -= writeDelay;
}
if (displayedStoryText.Length < originalStoryText.Length) if (i >= originalStoryText.Length)
{ {
displayedStoryText += originalStoryText.Substring(displayedStoryText.Length, 1); 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; yield return null;
} }
displayedStoryText = originalStoryText;
} }
public virtual void OnGUI() public virtual void OnGUI()
@ -178,21 +193,23 @@ namespace Fungus
} }
GUIStyle boxStyle = pageStyle.boxStyle; GUIStyle boxStyle = pageStyle.boxStyle;
GUIStyle titleStyle = pageStyle.GetScaledTitleStyle(); GUIStyle headerStyle = pageStyle.GetScaledHeaderStyle();
GUIStyle footerStyle = pageStyle.GetScaledFooterStyle();
GUIStyle sayStyle = pageStyle.GetScaledSayStyle(); GUIStyle sayStyle = pageStyle.GetScaledSayStyle();
GUIStyle optionStyle = pageStyle.GetScaledOptionStyle(); GUIStyle optionStyle = pageStyle.GetScaledOptionStyle();
GUIStyle optionAlternateStyle = pageStyle.GetScaledOptionAlternateStyle(); GUIStyle optionAlternateStyle = pageStyle.GetScaledOptionAlternateStyle();
GUIStyle continueStyle = pageStyle.GetScaledContinueStyle(); GUIStyle continueStyle = pageStyle.GetScaledContinueStyle();
Rect pageRect = GetScreenRect(); Rect pageRect = GetScreenRect();
Rect outerRect = pageRect; Rect outerRect = FitRectToScreen(pageRect);
Rect innerRect = CalcInnerRect(outerRect); Rect innerRect = CalcInnerRect(outerRect);
// Calculate height of each section // Calculate height of each section
float titleHeight = CalcTitleHeight(innerRect.width); float headerHeight = CalcHeaderHeight(innerRect.width);
float footerHeight = CalcFooterHeight(innerRect.width);
float storyHeight = CalcStoryHeight(innerRect.width); float storyHeight = CalcStoryHeight(innerRect.width);
float optionsHeight = CalcOptionsHeight(innerRect.width); float optionsHeight = CalcOptionsHeight(innerRect.width);
float contentHeight = titleHeight + storyHeight + optionsHeight; float contentHeight = headerHeight + footerHeight + storyHeight + optionsHeight;
// Adjust outer rect position based on alignment settings // Adjust outer rect position based on alignment settings
switch (verticalAlign) switch (verticalAlign)
@ -213,10 +230,7 @@ namespace Fungus
// Force outer rect to always be on-screen // 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 // If the rect is bigger than the screen, then the top-left corner will always be visible
outerRect.x = Mathf.Min(outerRect.x, Screen.width - outerRect.width); outerRect = FitRectToScreen(outerRect);
outerRect.y = Mathf.Min(outerRect.y, Screen.height - outerRect.height);
outerRect.x = Mathf.Max(0, outerRect.x);
outerRect.y = Mathf.Max(0, outerRect.y);
innerRect = CalcInnerRect(outerRect); innerRect = CalcInnerRect(outerRect);
@ -225,17 +239,29 @@ namespace Fungus
boxRect.height = contentHeight + (boxStyle.padding.top + boxStyle.padding.bottom); boxRect.height = contentHeight + (boxStyle.padding.top + boxStyle.padding.bottom);
GUI.Box(boxRect, "", boxStyle); GUI.Box(boxRect, "", boxStyle);
// Draw title label // Draw header label
Rect titleRect = innerRect; Rect headerRect = innerRect;
titleRect.height = titleHeight; headerRect.height = headerHeight;
GUI.Label(titleRect, titleText, titleStyle); if (headerHeight > 0)
{
GUI.Label(headerRect, headerText, headerStyle);
}
// Draw say label // Draw say label
Rect storyRect = innerRect; Rect storyRect = innerRect;
storyRect.y += titleHeight; storyRect.y += headerHeight;
storyRect.height = storyHeight; storyRect.height = storyHeight;
GUI.Label(storyRect, displayedStoryText, sayStyle); 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); bool finishedWriting = (displayedStoryText.Length == originalStoryText.Length);
if (!finishedWriting) if (!finishedWriting)
{ {
@ -259,7 +285,7 @@ namespace Fungus
{ {
// Draw option buttons // Draw option buttons
Rect buttonRect = innerRect; Rect buttonRect = innerRect;
buttonRect.y += titleHeight + storyHeight; buttonRect.y += headerHeight + storyHeight;
bool alternateRow = false; bool alternateRow = false;
foreach (Option option in options) foreach (Option option in options)
{ {
@ -329,21 +355,38 @@ namespace Fungus
} }
} }
float CalcTitleHeight(float boxWidth) float CalcHeaderHeight(float boxWidth)
{ {
PageStyle pageStyle = Game.GetInstance().activePageStyle; PageStyle pageStyle = Game.GetInstance().activePageStyle;
if (pageStyle == null || if (pageStyle == null ||
mode == Mode.Idle || mode == Mode.Idle ||
titleText.Length == 0) headerText.Length == 0)
{ {
return 0; return 0;
} }
GUIStyle titleStyle = pageStyle.GetScaledTitleStyle(); GUIStyle headerStyle = pageStyle.GetScaledHeaderStyle();
GUIContent titleContent = new GUIContent(titleText); GUIContent headerContent = new GUIContent(headerText);
return titleStyle.CalcHeight(titleContent, boxWidth); 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) float CalcStoryHeight(float boxWidth)
@ -392,6 +435,20 @@ namespace Fungus
return totalHeight; 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 // Returns smaller internal box rect with padding style applied
Rect CalcInnerRect(Rect outerRect) Rect CalcInnerRect(Rect outerRect)
{ {
@ -404,10 +461,12 @@ namespace Fungus
GUIStyle boxStyle = pageStyle.boxStyle; GUIStyle boxStyle = pageStyle.boxStyle;
return new Rect(outerRect.x + boxStyle.padding.left, Rect innerRect = new Rect(outerRect.x + boxStyle.padding.left,
outerRect.y + boxStyle.padding.top, outerRect.y + boxStyle.padding.top,
outerRect.width - (boxStyle.padding.left + boxStyle.padding.right), outerRect.width - (boxStyle.padding.left + boxStyle.padding.right),
outerRect.height - (boxStyle.padding.top + boxStyle.padding.bottom)); outerRect.height - (boxStyle.padding.top + boxStyle.padding.bottom));
return innerRect;
} }
Rect CalcContinueRect(Rect outerRect) Rect CalcContinueRect(Rect outerRect)
@ -431,8 +490,10 @@ namespace Fungus
return new Rect(x, y, width, height); return new Rect(x, y, width, height);
} }
// Returns the page rect in screen space coords /**
Rect GetScreenRect() * 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 // Y decreases up the screen in GUI space, so top left is rect origin
@ -448,31 +509,9 @@ namespace Fungus
Vector2 tl = mainCamera.WorldToScreenPoint(topLeft); Vector2 tl = mainCamera.WorldToScreenPoint(topLeft);
Vector2 br = mainCamera.WorldToScreenPoint(bottomRight); Vector2 br = mainCamera.WorldToScreenPoint(bottomRight);
return new Rect(tl.x, Screen.height - tl.y, br.x - tl.x, tl.y - br.y); Rect pageRect = new Rect(tl.x, Screen.height - tl.y, br.x - tl.x, tl.y - br.y);
}
// Inserts extra line breaks to avoid partial words 'jumping' to next line due to word wrap return FitRectToScreen(pageRect);
string InsertLineBreaks(string text, GUIStyle style, float maxWidth)
{
string output = "";
string[] parts = Regex.Split(text, @"(?=\s)");
foreach (string word in parts)
{
float oldHeight = style.CalcHeight(new GUIContent(output), maxWidth);
float newHeight = style.CalcHeight(new GUIContent(output + word), maxWidth);
if (oldHeight > 0 &&
newHeight > oldHeight)
{
output += "\n" + word.TrimStart();
}
else
{
output += word;
}
}
return output;
} }
} }
} }

34
Assets/Fungus/Scripts/PageStyle.cs

@ -12,20 +12,26 @@ namespace Fungus
// The font size for title, say and option text is calculated by multiplying the screen height // The font size for title, say and option text is calculated by multiplying the screen height
// by the corresponding font scale. Text appears the same size across all device resolutions. // by the corresponding font scale. Text appears the same size across all device resolutions.
/// Title font size as a fraction of screen height. /// Header font size as a fraction of screen height.
public float titleFontScale = 1f / 20f; public float headerFontScale = 1f / 20f;
/// Say font size as a fraction of screen height. /// Say font size as a fraction of screen height.
public float sayFontScale = 1f / 25f; public float sayFontScale = 1f / 25f;
/// Header font size as a fraction of screen height.
public float footerFontScale = 1f / 20f;
/// Continue font size as a fraction of screen height. /// Continue font size as a fraction of screen height.
public float continueFontScale = 1f / 30f; public float continueFontScale = 1f / 30f;
/// Option font size as a fraction of screen height. /// Option font size as a fraction of screen height.
public float optionFontScale = 1f / 25f; public float optionFontScale = 1f / 25f;
/// Style for title text /// Style for header text
public GUIStyle titleStyle; public GUIStyle headerStyle;
/// Style for header text
public GUIStyle footerStyle;
/// Style for say text /// Style for say text
public GUIStyle sayStyle; public GUIStyle sayStyle;
@ -43,14 +49,26 @@ namespace Fungus
public GUIStyle boxStyle; public GUIStyle boxStyle;
/** /**
* Returns the style for Title text. * Returns the style for Header text.
* Overrides the font size to compensate for varying device resolution.
* Font size is calculated as a fraction of the current screen height.
*/
public GUIStyle GetScaledHeaderStyle()
{
GUIStyle style = new GUIStyle(headerStyle);
style.fontSize = Mathf.RoundToInt((float)Screen.height * headerFontScale);
return style;
}
/**
* Returns the style for Footer text.
* Overrides the font size to compensate for varying device resolution. * Overrides the font size to compensate for varying device resolution.
* Font size is calculated as a fraction of the current screen height. * Font size is calculated as a fraction of the current screen height.
*/ */
public GUIStyle GetScaledTitleStyle() public GUIStyle GetScaledFooterStyle()
{ {
GUIStyle style = new GUIStyle(titleStyle); GUIStyle style = new GUIStyle(footerStyle);
style.fontSize = Mathf.RoundToInt((float)Screen.height * titleFontScale); style.fontSize = Mathf.RoundToInt((float)Screen.height * footerFontScale);
return style; return style;
} }

BIN
Assets/FungusExample/Scenes/Example.unity

Binary file not shown.

8
Assets/FungusExample/Scripts/PageRoom.cs

@ -16,8 +16,8 @@ public class PageRoom : Room
// You can also use the OnLeave() method to handle when the player leaves the room. // You can also use the OnLeave() method to handle when the player leaves the room.
void OnEnter() void OnEnter()
{ {
// Sets the title text on the page // Sets the header text on the page
Title("The Mushroom"); Header("The Mushroom");
// Each Say() command writes one line of text, followed by a continue button // Each Say() command writes one line of text, followed by a continue button
Say("One day in the forest, a mushroom grew."); Say("One day in the forest, a mushroom grew.");
@ -27,8 +27,8 @@ public class PageRoom : Room
// Wait for a few seconds // Wait for a few seconds
Wait(3); Wait(3);
// Set the title text to the empty string to remove the page title // Set the header text to the empty string to remove the page title
Title(""); Header("");
Say("..."); Say("...");
Say("Hmmm. Nothing seems to be happening."); Say("Hmmm. Nothing seems to be happening.");

Loading…
Cancel
Save