using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using Coroutine; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; 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; using TouchyTickets.Upgrades; namespace TouchyTickets { public class Ui { private static readonly BigInteger[] ExpoNums = Enumerable.Range(0, Localization.NumberFormat.Count).Select(i => BigInteger.Pow(1000, i + 1)).ToArray(); private readonly UiSystem uiSystem; private readonly Element[] swipeRelations; private Element currentUi; private float swipeProgress; private bool finishingSwipe; public Ui(UiSystem uiSystem) { this.uiSystem = uiSystem; foreach (var modifier in AttractionModifier.Modifiers.Values) this.uiSystem.TextFormatter.AddImage(modifier.Name, Assets.UiTexture[modifier.Texture]); foreach (var upgrade in Upgrade.Upgrades.Values) this.uiSystem.TextFormatter.AddImage(upgrade.Name, Assets.UiTexture[upgrade.Texture]); // main ticket store ui var rainingTickets = new List(); var main = new Group(Anchor.TopLeft, Vector2.One, false) { OnUpdated = (e, time) => { if (e.IsHidden) return; 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 / 30, Options.Instance.RainingTicketLimit)) 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 currentNews = Localization.GetRandomNews(); var newsTicker = main.AddChild(new Panel(Anchor.AutoCenter, new Vector2(1, 0.05F), Vector2.Zero)); newsTicker.AddChild(new Paragraph(Anchor.CenterLeft, float.MaxValue, p => currentNews, true) { OnUpdated = (e, time) => { e.PositionOffset -= new Vector2(1, 0); if (e.PositionOffset.X <= -e.DisplayArea.Width / e.Scale - 20) { e.PositionOffset = new Vector2(e.Parent.DisplayArea.Width / e.Scale + 20, 0); currentNews = Localization.GetRandomNews(); } } }); var ticketGroup = main.AddChild(new Group(Anchor.AutoCenter, new Vector2(1, 0.175F), false)); ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 10000, p => PrettyPrintNumber(GameImpl.Instance.Tickets) + "", true) { RegularFont = Assets.MonospacedFont, TextScale = 0.3F }); ticketGroup.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Map.TicketsPerSecond.ToString("#,0.##") + "/s", true)); BigInteger lastTickets = 0; ActiveCoroutine storeWobble = null; var storeGroup = main.AddChild(new CustomDrawGroup(Anchor.AutoCenter, new Vector2(1, 0.425F), 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, Assets.UiTexture[0, 0, 2, 3]) { OnPressed = e => { var rate = 1; if (Upgrade.TapIncrease[2].IsActive()) { rate = 50; } else if (Upgrade.TapIncrease[1].IsActive()) { rate = 10; } else if (Upgrade.TapIncrease[0].IsActive()) { rate = 5; } #if DEBUG rate = 500000; #endif GameImpl.Instance.Tickets += rate; }, CanBeSelected = true, CanBeMoused = true }); main.AddChild(new Group(Anchor.AutoLeft, new Vector2(1, 0.35F), false) { Padding = new Padding(6, 6, 12, 6), OnDrawn = (e, time, batch, alpha) => { var map = GameImpl.Instance.Map; var mapSize = new Vector2(map.Width, map.Height) * Assets.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 => { if (this.swipeProgress != 0) return; var map = GameImpl.Instance.Map; var infoUi = new Group(Anchor.BottomLeft, new Vector2(1)) {CanBeMoused = false}; AddSelectedAttractionInfo(infoUi); infoUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) { OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name)) }); infoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Remove")) { ActionSound = new SoundEffectInfo(Assets.PlaceSound), OnPressed = e2 => { if (map.SelectedPosition == null) return; map.Remove(map.SelectedPosition.Value); map.SelectedPosition = null; }, OnUpdated = (e2, time) => ((Button) e2).IsDisabled = map.SelectedPosition == null }); // we want this to render below the main ui while it fades away this.uiSystem.Add("MapViewInfo", infoUi).Priority = -100; this.FadeUi(true); } }); this.currentUi = main; this.uiSystem.Add("Main", main); // buy ui var buyUi = new Group(Anchor.TopLeft, Vector2.One, false) { IsHidden = true, OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha) }; buyUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Attractions"), true) {TextScale = 0.15F}); var buyList = buyUi.AddChild(new Panel(Anchor.AutoLeft, Vector2.One, Vector2.Zero, false, true, new Point(10, 30), false) { ChildPadding = new Padding(5, 15, 5, 5), PreventParentSpill = true }); foreach (var attraction in AttractionType.Attractions) { BigInteger price = 0; var attractionAmount = 0; var button = buyList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 40)) { ChildPadding = new Vector2(4), PositionOffset = new Vector2(0, 1), OnPressed = e => { if (this.swipeProgress != 0) return; 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 / Assets.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, 30), Localization.Get("Back")) { OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name)) }); yesNoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Place")) { ActionSound = new SoundEffectInfo(Assets.PlaceSound), OnPressed = e2 => { GameImpl.Instance.Tickets -= price; GameImpl.Instance.Platform.AddResourceEvent(true, "Tickets", (float) price, "Attraction", attraction.Key); map.Place(map.PlacingPosition, map.PlacingAttraction); map.PlacingAttraction.Wobble(); 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 attractionAmount = GameImpl.Instance.Map.GetAttractionAmount(attraction.Value); // yay compound interest price = (BigInteger) Math.Ceiling(attraction.Value.InitialPrice * Math.Pow(1 + 0.1F, attractionAmount)); } }); var image = button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), Assets.AttractionTexture[attraction.Value.TextureRegion]) { Padding = new Vector2(4) }); var right = button.AddChild(new Group(Anchor.TopRight, new Vector2(0.8F, 1), false) {CanBeMoused = false}); var name = right.AddChild(new Paragraph(Anchor.TopLeft, 1, Localization.Get(attraction.Key), true)); var hiddenName = right.AddChild(new Paragraph(Anchor.TopLeft, 1, "?????", true) {TextColor = Color.Gray}); var genRate = right.AddChild(new Paragraph(Anchor.BottomLeft, 1, p => attraction.Value.GetGenerationRate() + "/s", true) {TextScale = 0.08F}); right.AddChild(new Paragraph(Anchor.BottomRight, 1, p => PrettyPrintNumber(price) + "", true)); button.OnUpdated += (e, time) => { button.IsDisabled = GameImpl.Instance.Tickets < price; // we only want to show the info if we have enough tickets or we've placed the thing before var shouldShow = !button.IsDisabled || attractionAmount > 0; image.Color = shouldShow ? Color.White : Color.Black; name.IsHidden = !shouldShow; genRate.IsHidden = !shouldShow; hiddenName.IsHidden = shouldShow; }; } this.uiSystem.Add("Buy", buyUi); // modifier ui var modifierUi = new Group(Anchor.TopLeft, Vector2.One, false) { IsHidden = true, OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha) }; modifierUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Modifiers"), true) {TextScale = 0.15F}); var modifierList = modifierUi.AddChild(new Panel(Anchor.AutoLeft, Vector2.One, Vector2.Zero, false, true, new Point(10, 30), false) { ChildPadding = new Padding(5, 15, 5, 5), PreventParentSpill = true }); foreach (var modifier in AttractionModifier.Modifiers.Values) { var button = modifierList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) { SetHeightBasedOnChildren = true, PositionOffset = new Vector2(0, 1), ChildPadding = new Vector2(4), OnPressed = e => { if (this.swipeProgress != 0) return; var map = GameImpl.Instance.Map; map.PlacingModifier = modifier; var infoUi = new Group(Anchor.BottomLeft, new Vector2(1)) {CanBeMoused = false}; AddSelectedAttractionInfo(infoUi); infoUi.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) { OnPressed = e2 => this.FadeUi(false, () => this.uiSystem.Remove(e2.Root.Name)) }); var addButton = infoUi.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), string.Empty) { ActionSound = new SoundEffectInfo(Assets.PlaceSound), OnPressed = e2 => { if (map.SelectedPosition == null) return; var attraction = map.GetAttractionAt(map.SelectedPosition.Value); var placeAmount = 1; if (Upgrade.ModifierIncrease[2].IsActive()) { placeAmount = 10; } else if (Upgrade.ModifierIncrease[1].IsActive()) { placeAmount = 5; } else if (Upgrade.ModifierIncrease[0].IsActive()) { placeAmount = 3; } for (var i = 0; i < placeAmount; i++) { if (!map.PlacingModifier.Buy(attraction)) break; } attraction.Wobble(); }, OnUpdated = (e2, time) => { var disabled = map.SelectedPosition == null; if (!disabled) { var attraction = map.GetAttractionAt(map.SelectedPosition.Value); disabled = attraction == null || !map.PlacingModifier.IsAffected(attraction) || GameImpl.Instance.Tickets < attraction.GetModifierPrice(map.PlacingModifier); } ((Button) e2).IsDisabled = disabled; } }); addButton.Text.GetTextCallback = p => { BigInteger price = map.PlacingModifier.InitialPrice; if (map.SelectedPosition != null) { var attraction = map.GetAttractionAt(map.SelectedPosition.Value); if (attraction != null && map.PlacingModifier.IsAffected(attraction)) price = attraction.GetModifierPrice(map.PlacingModifier); } return PrettyPrintNumber(price) + ""; }; // we want this to render below the main ui while it fades away this.uiSystem.Add("ModifierInfo", infoUi).Priority = -100; this.FadeUi(true); } }); var image = button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), Assets.UiTexture[modifier.Texture]) { Padding = new Vector2(4) }); var right = button.AddChild(new Group(Anchor.TopRight, new Vector2(0.8F, 1)) {CanBeMoused = false}); var name = right.AddChild(new Paragraph(Anchor.TopLeft, 1, Localization.Get(modifier.Name), true)); var hiddenName = right.AddChild(new Paragraph(Anchor.TopLeft, 1, "?????", true) {TextColor = Color.Gray}); var desc = right.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get(modifier.Name + "Description"), true) {TextScale = 0.08F}); right.AddChild(new Paragraph(Anchor.AutoRight, 1, p => PrettyPrintNumber(modifier.InitialPrice) + "", true)); var genRate = right.AddChild(new Paragraph(Anchor.BottomLeft, 1, $"x{modifier.Multiplier}", true) {TextScale = 0.08F}); var shouldShow = false; button.OnAreaUpdated += e => { // this is a pretty expensive operation, so only do it when the area changes shouldShow = GameImpl.Instance.Map.IsAnyAttractionAffected(modifier); e.SetData("Show", shouldShow); }; button.OnUpdated += (e, time) => { button.IsDisabled = !shouldShow || GameImpl.Instance.Tickets < modifier.InitialPrice; image.Color = shouldShow ? Color.White : Color.Black; name.IsHidden = !shouldShow; hiddenName.IsHidden = shouldShow; genRate.IsHidden = !shouldShow; desc.IsHidden = !shouldShow; }; } modifierList.OnAreaUpdated += e => { // sort children by whether they should be shown or not modifierList.ReorderChildren((e1, e2) => Comparer.Default.Compare(e2.GetData("Show"), e1.GetData("Show"))); }; this.uiSystem.Add("Modifiers", modifierUi); // 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))); upgradeHeader.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => GameImpl.Instance.Stars + "", true) {TextScale = 0.3F}); upgradeHeader.AddChild(new Button(Anchor.AutoCenter, new Vector2(0.8F, 30), Localization.Get("EarnStar")) { PositionOffset = new Vector2(0, 4), OnUpdated = (e, time) => ((Button) e).IsDisabled = GameImpl.Instance.GetBuyableStars() <= 0, 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, string.Format(Localization.Get("ReallyEarnStar"), GameImpl.Instance.GetBuyableStars()))); panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) { OnPressed = e2 => this.uiSystem.Remove(e2.Root.Name) }); panel.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Yes")) { ActionSound = new SoundEffectInfo(Assets.BuySound), OnPressed = e2 => { this.uiSystem.Remove(e2.Root.Name); var game = GameImpl.Instance; game.Platform.AddResourceEvent(true, "Tickets", (float) game.Tickets, "Restart", "Restart" + game.TimesRestarted); game.Platform.AddResourceEvent(false, "Stars", game.GetBuyableStars(), "Restart", "Restart" + game.TimesRestarted); game.Stars += game.GetBuyableStars(); game.TimesRestarted++; game.Tickets = 0; game.Map = new ParkMap(game.Map.Width, game.Map.Height); } }); 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), Vector2.Zero, false, true, new Point(10, 30), false) { ChildPadding = new Padding(5, 15, 5, 5) }); upgradeHeader.OnAreaUpdated += e => upgradeList.Size = new Vector2(1, (upgradeUi.DisplayArea.Height - upgradeHeader.DisplayArea.Height) / e.Scale); PopulateUpgradeList(upgradeList); this.uiSystem.Add("Upgrade", upgradeUi); // options ui var optionsUi = new Group(Anchor.TopLeft, Vector2.One, false) { IsHidden = true, OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, ColorExtensions.FromHex(0xff86bccf) * alpha) }; optionsUi.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("Options"), true) {TextScale = 0.15F}); var optionList = optionsUi.AddChild(new Panel(Anchor.AutoLeft, new Vector2(1), Vector2.Zero, false, true, new Point(10, 30), false) { ChildPadding = new Padding(5, 15, 5, 5), PreventParentSpill = true }); optionList.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("GameplayOptions"), true) { TextScale = 0.12F }); optionList.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 20), Localization.Get("AutoBuyEnabled"), Options.Instance.AutoBuyEnabled) { OnCheckStateChange = (e, value) => { Options.Instance.AutoBuyEnabled = value; Options.Save(); }, PositionOffset = new Vector2(0, 1) }); optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get("MinTicketsForAutoBuy") + ":")); var num = optionList.AddChild(ElementHelper.NumberField(Anchor.AutoLeft, new Vector2(1, 20), Options.Instance.MinTicketsForAutoBuy, 1000, null, (t, value) => { if (int.TryParse(value, out var ret)) { Options.Instance.MinTicketsForAutoBuy = ret; Options.Save(); } })); num.PositionOffset = new Vector2(0, 1); optionList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 30), Localization.Get("Achievements")) { PositionOffset = new Vector2(0, 1), OnPressed = e => GameImpl.Instance.Platform.ShowAchievements() }); optionList.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("OtherOptions"), true) { PositionOffset = new Vector2(0, 10), TextScale = 0.12F }); optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, p => Localization.Get("RainingTicketLimit") + ": " + Options.Instance.RainingTicketLimit)); optionList.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 20), 10, 500) { PositionOffset = new Vector2(0, 1), CurrentValue = Options.Instance.RainingTicketLimit, OnValueChanged = (s, v) => { Options.Instance.RainingTicketLimit = (int) v; Options.Save(); } }); optionList.AddChild(new Paragraph(Anchor.AutoLeft, 1, p => Localization.Get("SoundVolume") + ": " + (int) (Options.Instance.SoundVolume * 100))); optionList.AddChild(new Slider(Anchor.AutoLeft, new Vector2(1, 20), 10, 100) { PositionOffset = new Vector2(0, 1), CurrentValue = Options.Instance.SoundVolume * 100, OnValueChanged = (s, v) => { Options.Instance.SoundVolume = v / 100; Options.Save(); } }); optionList.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 20), Localization.Get("WhileYouWereAwayMessage"), Options.Instance.WhileYouWereAwayMessage) { PositionOffset = new Vector2(0, 1), OnCheckStateChange = (b, value) => { Options.Instance.WhileYouWereAwayMessage = value; Options.Save(); } }); optionList.AddChild(new Checkbox(Anchor.AutoLeft, new Vector2(1, 20), Localization.Get("KeepScreenOn"), Options.Instance.KeepScreenOn) { PositionOffset = new Vector2(0, 1), OnCheckStateChange = (b, value) => { Options.Instance.KeepScreenOn = value; Options.Save(); } }); this.uiSystem.Add("Options", optionsUi); this.swipeRelations = new Element[] {optionsUi, upgradeUi, main, buyUi, modifierUi}; var swipeTimer = 0D; var swipeInfo = new Group(Anchor.BottomCenter, Vector2.One) { PositionOffset = new Vector2(0, 10), SetWidthBasedOnChildren = true, CanBeMoused = false, OnUpdated = (e, time) => { if (this.swipeProgress == 0) { if (swipeTimer > 0) { swipeTimer -= 0.04F; } else if (e.DrawAlpha > 0) { e.DrawAlpha -= 0.05F; } } else { swipeTimer = 1; if (e.DrawAlpha < 1) e.DrawAlpha += 0.05F; } } }; foreach (var ui in this.swipeRelations) { var tex = new TextureRegion(GameImpl.Instance.SpriteBatch.GetBlankTexture()); swipeInfo.AddChild(new Image(Anchor.AutoInlineIgnoreOverflow, new Vector2(12, 6), tex) { MaintainImageAspect = false, Padding = new Vector2(1), OnUpdated = (e, time) => ((Image) e).DrawAlpha = this.currentUi == ui ? 1 : 0.35F }); } this.uiSystem.Add("SwipeInfo", swipeInfo).Priority = 200; } public void Update(GameTime time) { // swiping between tabs if (!this.currentUi.IsHidden && this.uiSystem.Controls.HandleTouch && !this.currentUi.GetChildren(c => c.Horizontal && c.IsBeingScrolled, true).Any()) { 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.currentUi.Root.Transform = Matrix.Identity; this.finishingSwipe = false; this.swipeProgress = 0; } } var next = -1; if (this.swipeProgress != 0) { // actual swipe reaction logic var curr = Array.IndexOf(this.swipeRelations, this.currentUi); 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.currentUi.Root.Transform = Matrix.Identity; this.finishingSwipe = false; this.swipeProgress = 0; } } } 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); } for (var i = 0; i < this.swipeRelations.Length; i++) this.swipeRelations[i].IsHidden = i != next && this.swipeRelations[i] != this.currentUi; } } public static void SetupUiSystem(UiSystem uiSystem) { InputHandler.EnableGestures(GestureType.HorizontalDrag, GestureType.FreeDrag, GestureType.Pinch, GestureType.Tap); uiSystem.GlobalScale = 4; uiSystem.AutoScaleWithScreen = true; uiSystem.AutoScaleReferenceSize = new Point(720, 1280); uiSystem.Style.Font = Assets.Font; uiSystem.Style.PanelTexture = uiSystem.Style.TextFieldTexture = uiSystem.Style.ScrollBarBackground = uiSystem.Style.CheckboxTexture = new NinePatch(Assets.UiTexture[2, 1], 4); uiSystem.Style.ButtonTexture = uiSystem.Style.ScrollBarScrollerTexture = new NinePatch(Assets.UiTexture[3, 1], 4); uiSystem.Style.CheckboxCheckmark = Assets.UiTexture[4, 1]; uiSystem.Style.TextScale = 0.1F; uiSystem.Style.ActionSound = new SoundEffectInfo(Assets.ClickSound, 0.5F); uiSystem.TextFormatter.AddImage("ticket", Assets.UiTexture[2, 0]); uiSystem.TextFormatter.AddImage("star", Assets.UiTexture[3, 0]); } public static IEnumerator DisplaySplash(Action loadGame) { var splash = new Group(Anchor.TopLeft, Vector2.One, false) { OnDrawn = (e, time, batch, alpha) => batch.Draw(batch.GetBlankTexture(), e.DisplayArea, Color.Black * alpha) }; var content = splash.AddChild(new Group(Anchor.TopLeft, Vector2.One, false) {DrawAlpha = 0}); var center = content.AddChild(new Group(Anchor.Center, new Vector2(0.5F, 0.5F), false)); center.AddChild(new Image(Anchor.AutoCenter, new Vector2(1, -1), Assets.UiTexture[4, 0])); center.AddChild(new Paragraph(Anchor.AutoCenter, 10000, Localization.Get("AGameByEllpeck"), true)); content.AddChild(new Paragraph(Anchor.BottomCenter, 1, Localization.Get("TranslationBy"), true) { PositionOffset = new Vector2(0, 15), TextScale = 0.08F }); GameImpl.Instance.UiSystem.Add("Splash", splash).Priority = 100000; while (content.DrawAlpha < 1) { content.DrawAlpha += 0.015F; yield return new Wait(CoroutineEvents.Update); } yield return new Wait(0.5); var analyticsFlag = new FileInfo(Path.Combine(SaveHandler.GetGameDirectory(true).FullName, "_ReadGdpr")); if (!analyticsFlag.Exists) { var evt = new Event(); var panel = splash.AddChild(new Panel(Anchor.Center, new Vector2(0.8F), Vector2.Zero, true)); panel.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get("GDPRInfo"))); panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 30), Localization.Get("Okay")) { OnPressed = e2 => { // create the (empty) flag file using (analyticsFlag.Create()) { } splash.RemoveChild(panel); CoroutineHandler.RaiseEvent(evt); } }); yield return new Wait(evt); } yield return new Wait(0.25); loadGame(); yield return new Wait(0.25); while (content.DrawAlpha > 0) { content.DrawAlpha -= 0.015F; yield return new Wait(CoroutineEvents.Update); } while (splash.DrawAlpha > 0) { splash.DrawAlpha -= 0.015F; yield return new Wait(CoroutineEvents.Update); } splash.System.Remove(splash.Root.Name); } public static void DisplayWhileYouWereAway(TimeSpan passed, BigInteger ticketGen, double ticketPerSecondGen) { if (ticketGen <= 0 && ticketPerSecondGen <= 0) return; 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)); var text = string.Format(Localization.Get("WhileYouWereAway"), passed.TotalMinutes.ToString("0.#")); if (ticketGen > 0) text += " " + string.Format(Localization.Get("WhileYouWereAwayTickets"), PrettyPrintNumber(ticketGen)); if (ticketPerSecondGen > 0) text += " " + string.Format(Localization.Get("WhileYouWereAwayTps"), ticketPerSecondGen.ToString("#,0.##")); panel.AddChild(new Paragraph(Anchor.AutoLeft, 1, text)); panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(1, 30), Localization.Get("Okay")) { OnPressed = e2 => e2.System.Remove(e2.Root.Name) }); GameImpl.Instance.UiSystem.Add("WhileYouWereAway", infoBox); } public static void DisplayRatePlease() { 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, string.Format(Localization.Get("RateInfo")))); panel.AddChild(new Button(Anchor.AutoLeft, new Vector2(0.5F, 30), Localization.Get("Back")) { OnPressed = e2 => e2.System.Remove(e2.Root.Name) }); panel.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(0.5F, 30), Localization.Get("Rate")) { OnPressed = e2 => { GameImpl.Instance.Platform.OpenRateLink(); e2.System.Remove(e2.Root.Name); } }); GameImpl.Instance.UiSystem.Add("RatePlease", infoBox); } 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 Wait(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) { var map = GameImpl.Instance.Map; map.PlacingAttraction = null; map.SelectedPosition = null; map.PlacingModifier = null; } after?.Invoke(); } CoroutineHandler.Start(Impl()); } private static void AddSelectedAttractionInfo(Element element) { var map = GameImpl.Instance.Map; element.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => { if (map.SelectedPosition == null) return string.Empty; var attraction = map.GetAttractionAt(map.SelectedPosition.Value); return string.Join(" ", attraction.Modifiers.Select(m => $"{m.Amount}")); }, true)); element.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => { if (map.SelectedPosition == null) return string.Empty; var pos = map.SelectedPosition.Value; var attraction = map.GetAttractionAt(pos); // beautiful return string.Join(" ", Upgrade.Upgrades.Values .Select(u => { var multiplier = u is ModifierUpgrade mod ? mod.GetCurrentMultiplier(attraction.Type) : u is NeighborModifierUpgrade neighbor ? neighbor.GetCurrentMultiplier(attraction, map, pos) : 1; return multiplier > 1 ? $" x{multiplier}" : null; }).Where(s => s != null)); }, true)); element.AddChild(new Paragraph(Anchor.AutoCenter, 1, p => { if (map.SelectedPosition == null) return string.Empty; var pos = map.SelectedPosition.Value; var attraction = map.GetAttractionAt(pos); return attraction.GetGenerationRate(map, pos).ToString("#,0.##") + "/s"; }, true)); } private static void PopulateUpgradeList(Element upgradeList) { upgradeList.RemoveChildren(c => !(c is ScrollBar)); var reachedActive = false; foreach (var upgrade in Upgrade.Upgrades.Values.OrderBy(u => u.IsActive())) { // the first active upgrade should be preceded by a section separator paragraph if (!reachedActive && upgrade.IsActive()) { reachedActive = true; upgradeList.AddChild(new Paragraph(Anchor.AutoCenter, 1, Localization.Get("AppliedUpgrades"), true) { PositionOffset = new Vector2(0, 4), TextScale = 0.12F }); } var button = upgradeList.AddChild(new Button(Anchor.AutoLeft, new Vector2(1)) { SetHeightBasedOnChildren = true, PositionOffset = new Vector2(0, 1), ChildPadding = new Vector2(4), ActionSound = new SoundEffectInfo(Assets.BuySound), OnPressed = e => { GameImpl.Instance.Stars -= upgrade.Price; GameImpl.Instance.Platform.AddResourceEvent(true, "Stars", upgrade.Price, "Upgrade", upgrade.Name); GameImpl.Instance.AppliedUpgrades.Add(upgrade); upgrade.OnApplied(); PopulateUpgradeList(upgradeList); } }); void HideAndDisable() { button.IsHidden = upgrade.Dependencies.Any(u => !u.IsActive()); button.IsDisabled = upgrade.IsActive() || GameImpl.Instance.Stars < upgrade.Price; } button.OnUpdated += (e, time) => HideAndDisable(); HideAndDisable(); button.AddChild(new Image(Anchor.CenterLeft, new Vector2(0.2F, 40), Assets.UiTexture[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.TopLeft, 1, Localization.Get(upgrade.Name), true)); if (!reachedActive) right.AddChild(new Paragraph(Anchor.TopRight, 1, p => upgrade.Price + "", true)); right.AddChild(new Paragraph(Anchor.AutoLeft, 1, Localization.Get(upgrade.Name + "Description"), true) {TextScale = 0.08F}); } } 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 Wait(CoroutineEvents.Update); } element.Transform = Matrix.Identity; } public static string PrettyPrintNumber(BigInteger number) { for (var i = 0; i < Localization.NumberFormat.Count; i++) { if (number < ExpoNums[i]) return number.ToString(Localization.NumberFormat[i]); } // if the number is too large, just return the highest possible return number.ToString(Localization.NumberFormat.Last()); } } }