342 lines
No EOL
19 KiB
C#
342 lines
No EOL
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using Coroutine;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using Microsoft.Xna.Framework.Input.Touch;
|
|
using MLEM.Extensions;
|
|
using MLEM.Font;
|
|
using MLEM.Formatting.Codes;
|
|
using MLEM.Input;
|
|
using MLEM.Misc;
|
|
using MLEM.Startup;
|
|
using MLEM.Textures;
|
|
using MLEM.Ui;
|
|
using MLEM.Ui.Elements;
|
|
using TouchyTickets.Attractions;
|
|
|
|
namespace TouchyTickets {
|
|
public class Ui {
|
|
|
|
public static readonly UniformTextureAtlas Texture = new UniformTextureAtlas(MlemGame.LoadContent<Texture2D>("Textures/Ui"), 16, 16);
|
|
private readonly UiSystem uiSystem;
|
|
private readonly Element[] swipeRelations;
|
|
private Element currentUi;
|
|
private float swipeProgress;
|
|
private bool finishingSwipe;
|
|
|
|
public Ui(UiSystem uiSystem) {
|
|
InputHandler.EnableGestures(GestureType.HorizontalDrag, GestureType.FreeDrag, GestureType.Pinch, GestureType.Tap);
|
|
this.uiSystem = uiSystem;
|
|
this.uiSystem.GlobalScale = 4;
|
|
this.uiSystem.AutoScaleWithScreen = true;
|
|
this.uiSystem.AutoScaleReferenceSize = new Point(720, 1280);
|
|
this.uiSystem.Style.Font = new GenericSpriteFont(MlemGame.LoadContent<SpriteFont>("Fonts/Regular"));
|
|
this.uiSystem.Style.PanelTexture = this.uiSystem.Style.ScrollBarBackground = new NinePatch(Texture[2, 1], 4);
|
|
this.uiSystem.Style.ButtonTexture = this.uiSystem.Style.ScrollBarScrollerTexture = new NinePatch(Texture[3, 1], 4);
|
|
this.uiSystem.Style.TextScale = 0.1F;
|
|
this.uiSystem.TextFormatter.AddImage("ticket", Texture[2, 0]);
|
|
this.uiSystem.TextFormatter.AddImage("star", Texture[3, 0]);
|
|
|
|
// main ticket store ui
|
|
var rainingTickets = new List<RainingTicket>();
|
|
var main = new Group(Anchor.TopLeft, Vector2.One, false) {
|
|
OnUpdated = (e, time) => {
|
|
for (var i = rainingTickets.Count - 1; i >= 0; i--) {
|
|
if (rainingTickets[i].Update())
|
|
rainingTickets.RemoveAt(i);
|
|
}
|
|
while (rainingTickets.Count < Math.Min(GameImpl.Instance.Map.TicketsPerSecond / 10, 200))
|
|
rainingTickets.Add(new RainingTicket());
|
|
},
|
|
OnDrawn = (e, time, batch, alpha) => {
|
|
batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha);
|
|
foreach (var ticket in rainingTickets)
|
|
ticket.Draw(batch, e.DisplayArea.Size, e.Scale, Color.White * alpha);
|
|
}
|
|
};
|
|
var ticketGroup = main.AddChild(new Group(Anchor.AutoCenter, new Vector2(1, 0.15F), false));
|
|
ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 10000, p => PrettyPrintNumber(GameImpl.Instance.Tickets) + "<i ticket>", true) {
|
|
TextScale = 0.3F
|
|
});
|
|
ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Map.TicketsPerSecond.ToString("0.##") + "<i ticket>/s", true) {
|
|
PositionOffset = new Vector2(0, -8)
|
|
});
|
|
BigInteger lastTickets = 0;
|
|
ActiveCoroutine storeWobble = null;
|
|
var storeGroup = main.AddChild(new CustomDrawGroup(Anchor.AutoCenter, new Vector2(1, 0.5F), null, null, false) {
|
|
OnUpdated = (e, time) => {
|
|
if (lastTickets != GameImpl.Instance.Tickets) {
|
|
lastTickets = GameImpl.Instance.Tickets;
|
|
// only wobble if we're not already wobbling
|
|
if (storeWobble == null || storeWobble.IsFinished)
|
|
storeWobble = CoroutineHandler.Start(WobbleElement((CustomDrawGroup) e));
|
|
}
|
|
}
|
|
});
|
|
storeGroup.AddChild(new Image(Anchor.TopLeft, Vector2.One, Texture[0, 0, 2, 3]) {
|
|
OnPressed = e => {
|
|
#if DEBUG
|
|
GameImpl.Instance.Tickets += 500000;
|
|
#else
|
|
GameImpl.Instance.Tickets++;
|
|
#endif
|
|
},
|
|
CanBeSelected = true,
|
|
CanBeMoused = true
|
|
});
|
|
main.AddChild(new Group(Anchor.AutoLeft, new Vector2(1, 0.35F), false) {
|
|
Padding = new Padding(4, 4, 12, 4),
|
|
OnDrawn = (e, time, batch, alpha) => {
|
|
var map = GameImpl.Instance.Map;
|
|
var mapSize = new Vector2(map.Width, map.Height) * Attraction.TileSize;
|
|
var (scaleX, scaleY) = e.DisplayArea.Size / mapSize;
|
|
var scale = Math.Min(scaleX, scaleY);
|
|
var pos = e.DisplayArea.Location + (e.DisplayArea.Size - mapSize * scale) / 2;
|
|
batch.Draw(this.uiSystem.Style.PanelTexture, new RectangleF(pos - new Vector2(2) * e.Scale, mapSize * scale + new Vector2(4) * e.Scale), Color.White * alpha, e.Scale);
|
|
map.Draw(time, batch, pos, scale, alpha, false, new RectangleF(Vector2.Zero, mapSize * scale));
|
|
},
|
|
OnPressed = e => {
|
|
var backUi = new Group(Anchor.BottomLeft, new Vector2(1));
|
|
backUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(1, 20), Localization.Get("Back")) {
|
|
OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name))
|
|
});
|
|
// we want this to render below the main ui while it fades away
|
|
this.uiSystem.Add("MapViewBack", backUi).Priority = -100;
|
|
|
|
this.FadeUi(true);
|
|
}
|
|
});
|
|
this.currentUi = main;
|
|
this.uiSystem.Add("Main", main);
|
|
|
|
// buy ui
|
|
var buyUi = new Panel(Anchor.TopLeft, Vector2.One, Vector2.Zero, false, true, new Point(10, 30), false) {
|
|
ChildPadding = new Padding(5, 15, 5, 5),
|
|
IsHidden = true
|
|
};
|
|
foreach (var attraction in AttractionType.Attractions) {
|
|
BigInteger price = 0;
|
|
var button = buyUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 40)) {
|
|
ChildPadding = new Vector2(4),
|
|
PositionOffset = new Vector2(0, 1),
|
|
OnPressed = e => {
|
|
var map = GameImpl.Instance.Map;
|
|
map.PlacingAttraction = attraction.Value.Create();
|
|
// set placing position to center of camera's view
|
|
var (posX, posY) = (GameImpl.Instance.Camera.LookingPosition / Attraction.TileSize).ToPoint();
|
|
map.PlacingPosition = new Point(MathHelper.Clamp(posX, 0, map.Width - attraction.Value.Width), MathHelper.Clamp(posY, 0, map.Height - attraction.Value.Height));
|
|
|
|
var yesNoUi = new Group(Anchor.BottomLeft, new Vector2(1));
|
|
yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 20), Localization.Get("Back")) {
|
|
OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name))
|
|
});
|
|
yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 20), Localization.Get("Place")) {
|
|
OnPressed = e2 => {
|
|
GameImpl.Instance.Tickets -= price;
|
|
map.Place(map.PlacingPosition, map.PlacingAttraction);
|
|
this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name));
|
|
},
|
|
OnUpdated = (e2, time) => ((Button) e2).IsDisabled = !map.CanPlace(map.PlacingPosition, map.PlacingAttraction)
|
|
});
|
|
// we want this to render below the main ui while it fades away
|
|
this.uiSystem.Add("PlacingYesNo", yesNoUi).Priority = -100;
|
|
|
|
this.FadeUi(true);
|
|
},
|
|
OnAreaUpdated = e => {
|
|
// only update the price when the area updates, since it won't change while we're in the buy ui
|
|
var amount = GameImpl.Instance.Map.GetAttractionAmount(attraction.Value);
|
|
// yay compound interest
|
|
price = (attraction.Value.InitialPrice * (float) Math.Pow(1 + 0.1F, amount)).Ceil();
|
|
}
|
|
});
|
|
button.OnUpdated += (e, time) => {
|
|
button.IsDisabled = GameImpl.Instance.Tickets < price;
|
|
};
|
|
var center = button.AddChild(new Group(Anchor.Center, new Vector2(0.8F, 1), false) {CanBeMoused = false});
|
|
center.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get(attraction.Key), true));
|
|
center.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => attraction.Value.GetGenerationRate() + "<i ticket>/s", true) {TextScale = 0.08F});
|
|
button.AddChild(new Image(Anchor.CenterLeft, new Vector2(40), attraction.Value.TextureRegion) {
|
|
Padding = new Vector2(4)
|
|
});
|
|
button.AddChild(new Paragraph(Anchor.CenterRight, 1, p => PrettyPrintNumber(price) + "<i ticket>", true));
|
|
}
|
|
this.uiSystem.Add("Buy", buyUi);
|
|
|
|
// upgrade ui
|
|
var upgradeUi = new Group(Anchor.TopLeft, Vector2.One, false) {
|
|
IsHidden = true,
|
|
OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha)
|
|
};
|
|
var upgradeHeader = upgradeUi.AddChild(new Group(Anchor.AutoLeft, new Vector2(1, 0.25F), false));
|
|
upgradeHeader.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Stars + "<i star>", true) {TextScale = 0.3F});
|
|
upgradeHeader.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.8F, 20), Localization.Get("EarnStar")) {
|
|
PositionOffset = new Vector2(0, 4),
|
|
OnUpdated = (e, time) => ((Button) e).IsDisabled = GameImpl.Instance.Tickets < GameImpl.Instance.GetStarPrice(),
|
|
OnPressed = e => {
|
|
var infoBox = new Group(Anchor.TopLeft, Vector2.One, false) {
|
|
OnDrawn = (e2, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e2.DisplayArea, Color.Black * 0.35F)
|
|
};
|
|
var panel = infoBox.AddChild(new Panel(Anchor.Center, new Vector2(0.8F), Vector2.Zero, true));
|
|
panel.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get("ReallyEarnStar")));
|
|
panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 20), Localization.Get("Back")) {
|
|
OnPressed = e2 => this.uiSystem.Remove(e2.Root.Name)
|
|
});
|
|
panel.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 20), Localization.Get("Yes")) {
|
|
OnPressed = e2 => {
|
|
this.uiSystem.Remove(e2.Root.Name);
|
|
var game = GameImpl.Instance;
|
|
game.TimesRestarted++;
|
|
game.Stars++;
|
|
game.Map = new ParkMap(game.Map.Width, game.Map.Height);
|
|
game.Tickets = 0;
|
|
}
|
|
});
|
|
this.uiSystem.Add("ReallyEarnStarBox", infoBox);
|
|
}
|
|
});
|
|
upgradeHeader.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => string.Format(Localization.Get("RequiresTickets"), PrettyPrintNumber(GameImpl.Instance.GetStarPrice())), true) {
|
|
PositionOffset = new Vector2(0, 2)
|
|
});
|
|
var upgradeList = upgradeUi.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1, 0.75F), Vector2.Zero, false, true, new Point(10, 30), false) {
|
|
ChildPadding = new Padding(5, 15, 5, 5)
|
|
});
|
|
foreach (var upgrade in Upgrade.Upgrades.Values) {
|
|
var button = upgradeList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) {
|
|
SetHeightBasedOnChildren = true,
|
|
PositionOffset = new Vector2(0, 1),
|
|
ChildPadding = new Vector2(4),
|
|
OnPressed = e => {
|
|
GameImpl.Instance.Stars--;
|
|
GameImpl.Instance.AppliedUpgrades.Add(upgrade);
|
|
upgrade.OnApplied();
|
|
}
|
|
});
|
|
button.OnUpdated += (e, time) => {
|
|
// we want to hide if we're active, or if we still need a dependency to be active
|
|
button.IsHidden = upgrade.IsActive() || upgrade.Dependencies.Any(u => !u.IsActive());
|
|
button.IsDisabled = GameImpl.Instance.Stars < upgrade.Price;
|
|
};
|
|
button.AddChild(new Paragraph(Anchor.TopCenter, 1, Localization.Get(upgrade.Name), true));
|
|
button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), upgrade.Texture) {
|
|
Padding = new Vector2(4)
|
|
});
|
|
var right = button.AddChild(new Group(Anchor.TopRight, new Vector2(0.8F, 1)) {CanBeMoused = false});
|
|
right.AddChild(new Paragraph(Anchor.TopRight, 1, p => upgrade.Price + "<i star>", true));
|
|
right.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get(upgrade.Name + "Description"), true) {TextScale = 0.08F});
|
|
}
|
|
this.uiSystem.Add("Upgrade", upgradeUi);
|
|
|
|
this.swipeRelations = new Element[] {upgradeUi, main, buyUi};
|
|
}
|
|
|
|
public void Update(GameTime time) {
|
|
// swiping between tabs
|
|
if (!this.currentUi.IsHidden && this.uiSystem.Controls.HandleTouch) {
|
|
if (MlemGame.Input.GetGesture(GestureType.HorizontalDrag, out var gesture)) {
|
|
this.swipeProgress -= gesture.Delta.X / this.currentUi.DisplayArea.Width;
|
|
} else if (!this.finishingSwipe && this.swipeProgress != 0 && !MlemGame.Input.TouchState.Any()) {
|
|
// if we're not dragging or holding, we need to move back to our current ui
|
|
this.swipeProgress -= Math.Sign(this.swipeProgress) * 0.03F;
|
|
if (this.swipeProgress.Equals(0, 0.03F))
|
|
this.ResetSwipe();
|
|
}
|
|
|
|
if (this.swipeProgress != 0) {
|
|
// actual swipe reaction logic
|
|
var curr = Array.IndexOf(this.swipeRelations, this.currentUi);
|
|
var next = curr + Math.Sign(this.swipeProgress);
|
|
if (next >= 0 && next < this.swipeRelations.Length) {
|
|
this.swipeRelations[next].IsHidden = false;
|
|
// if we've swiped a bit, we count this as a success and move to the next ui
|
|
this.finishingSwipe = Math.Abs(this.swipeProgress) >= 0.15F;
|
|
// if we're in the process of finishing a swipe, move without requiring input
|
|
if (this.finishingSwipe) {
|
|
if (!MlemGame.Input.TouchState.Any())
|
|
this.swipeProgress += Math.Sign(this.swipeProgress) * 0.05F;
|
|
// we're done with the swipe, so switch the active ui
|
|
if (this.swipeProgress.Equals(Math.Sign(this.swipeProgress), 0.05F)) {
|
|
this.currentUi = this.swipeRelations[next];
|
|
this.ResetSwipe();
|
|
}
|
|
}
|
|
} else {
|
|
// when we're swiping into the void, we want to stop at the max
|
|
this.swipeProgress = MathHelper.Clamp(this.swipeProgress, -1, 1);
|
|
}
|
|
|
|
// update element positions
|
|
this.currentUi.Root.Transform = Matrix.CreateTranslation(-this.swipeProgress * this.currentUi.DisplayArea.Width, 0, 0);
|
|
if (next >= 0 && next < this.swipeRelations.Length)
|
|
this.swipeRelations[next].Root.Transform = Matrix.CreateTranslation((Math.Sign(this.swipeProgress) - this.swipeProgress) * this.swipeRelations[next].DisplayArea.Width, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ResetSwipe() {
|
|
this.finishingSwipe = false;
|
|
this.swipeProgress = 0;
|
|
foreach (var element in this.swipeRelations) {
|
|
element.IsHidden = element != this.currentUi;
|
|
element.Root.Transform = Matrix.Identity;
|
|
}
|
|
}
|
|
|
|
private void FadeUi(bool fadeOut, Action after = null) {
|
|
IEnumerator<IWait> Impl() {
|
|
// disable input handling during fade
|
|
this.uiSystem.Controls.HandleTouch = false;
|
|
GameImpl.Instance.DrawMap = true;
|
|
this.currentUi.IsHidden = false;
|
|
var alpha = 1F;
|
|
while (alpha > 0) {
|
|
alpha -= 0.03F;
|
|
this.currentUi.DrawAlpha = !fadeOut ? 1 - alpha : alpha;
|
|
yield return new WaitEvent(CoroutineEvents.Update);
|
|
}
|
|
this.uiSystem.Controls.HandleTouch = true;
|
|
GameImpl.Instance.DrawMap = fadeOut;
|
|
this.currentUi.IsHidden = fadeOut;
|
|
// disable horizontal and vertical drag on map view to allow free drag to take priority
|
|
InputHandler.SetGesturesEnabled(!fadeOut, GestureType.HorizontalDrag, GestureType.VerticalDrag);
|
|
if (!fadeOut)
|
|
GameImpl.Instance.Map.PlacingAttraction = null;
|
|
after?.Invoke();
|
|
}
|
|
|
|
CoroutineHandler.Start(Impl());
|
|
}
|
|
|
|
private static IEnumerator<IWait> WobbleElement(CustomDrawGroup element, float intensity = 0.02F) {
|
|
var sin = 0F;
|
|
while (sin < MathHelper.Pi) {
|
|
element.ScaleOrigin(1 + (float) Math.Sin(sin) * intensity);
|
|
sin += 0.2F;
|
|
yield return new WaitEvent(CoroutineEvents.Update);
|
|
}
|
|
element.Transform = Matrix.Identity;
|
|
}
|
|
|
|
private static string PrettyPrintNumber(BigInteger number) {
|
|
if (number < 1000)
|
|
return number.ToString();
|
|
// thousands
|
|
if (number < 1000000)
|
|
return number.ToString("0,.##K");
|
|
// millions
|
|
if (number < 1000000000)
|
|
return number.ToString("0,,.##M");
|
|
// billions
|
|
if (number < 1000000000000)
|
|
return number.ToString("0,,,.##B");
|
|
// trillions
|
|
if (number < 1000000000000000)
|
|
return number.ToString("0,,,,.##T");
|
|
return number.ToString("0,,,,,.##Q");
|
|
}
|
|
|
|
}
|
|
} |