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 ThemeParkClicker.Attractions; namespace ThemeParkClicker { public class Ui { public static readonly UniformTextureAtlas Texture = new UniformTextureAtlas(MlemGame.LoadContent("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("Fonts/Regular")); this.uiSystem.Style.TextScale = 0.1F; this.uiSystem.TextFormatter.AddImage("ticket", Texture[2, 0]); // main ticket store ui var rainingTickets = new List(); 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, 500)) rainingTickets.Add(new RainingTicket()); }, OnDrawn = (e, time, batch, alpha) => { batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86cfcb) * 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, 1, p => GameImpl.Instance.DisplayTicketCount(), true) { TextScale = 0.3F }); ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Map.TicketsPerSecond.ToString("0.##") + "/s", true) { PositionOffset = new Vector2(0, -8) }); BigInteger lastTickets = 0; 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; CoroutineHandler.Start(WobbleElement((CustomDrawGroup) e)); } } }); storeGroup.AddChild(new Image(Anchor.TopLeft, Vector2.One, Texture[0, 0, 2, 3]) { OnPressed = e => GameImpl.Instance.Tickets++, 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; map.Draw(time, batch, pos, scale, alpha, 0); }, OnPressed = e => { var map = GameImpl.Instance.Map; GameImpl.Instance.Camera.LookingPosition = new Vector2(map.Width, map.Height) / 2 * Attraction.TileSize; var backUi = new Group(Anchor.BottomLeft, new Vector2(1)); backUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(1, 40), "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), Padding = new Padding(0, 0, 0, 4), OnPressed = e => { var map = GameImpl.Instance.Map; var pos = new Vector2(map.Width, map.Height) / 2; GameImpl.Instance.Camera.LookingPosition = (pos + new Vector2(0.5F)) * Attraction.TileSize; map.PlacingAttraction = attraction.Value.Create(); map.PlacingPosition = pos.ToPoint(); var yesNoUi = new Group(Anchor.BottomLeft, new Vector2(1)); yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 40), "Back") { OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name)) }); yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 40), "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, attraction.Key, true)); center.AddChild(new Paragraph(Anchor.AutoCenter, 1, attraction.Value.GenerationPerSecond + "/s", true) {TextScale = 0.08F}); var image = button.AddChild(new Image(Anchor.CenterLeft, new Vector2(1), attraction.Value.TextureRegion) { Padding = new Vector2(4) }); button.OnAreaUpdated += e => image.Size = new Vector2(e.DisplayArea.Height / e.Scale); button.AddChild(new Paragraph(Anchor.CenterRight, 1, p => price + "", true)); } this.uiSystem.Add("Buy", buyUi); this.swipeRelations = new Element[] {main, buyUi}; } public void Update(GameTime time) { // swiping between tabs if (!this.currentUi.IsHidden) { 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 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 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; } } }