using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input.Touch; using MLEM.Extensions; using MLEM.Misc; using MLEM.Startup; using MLEM.Textures; using TouchyTickets.Attractions; using TouchyTickets.Upgrades; namespace TouchyTickets; [DataContract] public class ParkMap { private const int AdditionalRadius = 15; private const int AutoBuyIntervalSecs = 30; [DataMember] public readonly int Width; [DataMember] public readonly int Height; [DataMember] private readonly List<(Point, Attraction)> attractions = new(); private readonly Dictionary treePositions = new(); private readonly Dictionary fencePositions = new(); private readonly Attraction[,] attractionGrid; [DataMember] public double TicketsPerSecond { get; private set; } public Attraction PlacingAttraction; public AttractionModifier PlacingModifier; public Point PlacingPosition; public Point? SelectedPosition; private bool draggingAttraction; private double autoBuyCounter; public ParkMap(int width, int height) { this.Width = width; this.Height = height; this.attractionGrid = new Attraction[width, height]; // set up trees var random = new Random(); for (var x = -ParkMap.AdditionalRadius; x < this.Width + ParkMap.AdditionalRadius; x++) { for (var y = -ParkMap.AdditionalRadius; y < this.Height + ParkMap.AdditionalRadius; y++) { var pos = new Point(x, y); if (this.IsInBounds(pos)) continue; if (random.Next(15) != 0) continue; var type = random.Next(3); this.treePositions[pos] = type; } } // set up fences this.fencePositions[new Point(-1, -1)] = 2; this.fencePositions[new Point(this.Width, -1)] = 3; this.fencePositions[new Point(-1, this.Height)] = 4; this.fencePositions[new Point(this.Width, this.Height)] = 5; for (var x = 0; x < this.Width; x++) { this.fencePositions[new Point(x, -1)] = 0; this.fencePositions[new Point(x, this.Height)] = 0; } for (var y = 0; y < this.Height; y++) { this.fencePositions[new Point(-1, y)] = 1; this.fencePositions[new Point(this.Width, y)] = 1; } } public void Update(TimeSpan passed, bool wasAway) { var toSimulate = wasAway ? new TimeSpan(passed.Ticks / 2) : passed; // handle auto-buying this.autoBuyCounter += toSimulate.TotalSeconds; this.TryAutoBuy(); var autoBuysPerAttraction = ((float) this.autoBuyCounter / ParkMap.AutoBuyIntervalSecs / this.attractions.Count).Ceil(); // update tickets this.TicketsPerSecond = 0; foreach (var (pos, attraction) in this.attractions) { var genPerSecond = attraction.Update(toSimulate, this, pos); this.TicketsPerSecond += genPerSecond; // if we were away, we have to catch up with auto-buys while also taking into account // the amount of tickets that each ride generates. The easiest way we can do this is // to progress, between updating each ride, by a percentage of the total update amount if (wasAway) { for (var i = autoBuysPerAttraction; i > 0; i--) { if (!this.TryAutoBuy()) break; } } } // map movement if (GameImpl.Instance.DrawMap && GameImpl.Instance.UiSystem.Controls.HandleTouch) { var camera = GameImpl.Instance.Camera; if (MlemGame.Input.GetGesture(GestureType.Pinch, out var pinch)) { // pinch zoom var center = (pinch.Position + pinch.Position2) / 2; var newDist = Vector2.Distance(pinch.Position + pinch.Delta, pinch.Position2 + pinch.Delta2); var oldDist = Vector2.Distance(pinch.Position, pinch.Position2); var newScale = newDist / oldDist * camera.Scale; camera.Zoom(newScale - camera.Scale, center); } else if (MlemGame.Input.GetGesture(GestureType.FreeDrag, out var drag)) { if (this.draggingAttraction) { // move the current placing position var nextPos = (camera.ToWorldPos(drag.Position + drag.Delta) / Assets.TileSize).ToPoint(); // drag the center of the attraction nextPos -= new Point(this.PlacingAttraction.Type.Width / 2, this.PlacingAttraction.Type.Height / 2); if (this.PlacingAttraction.Type.GetCoveredTiles().Select(p => nextPos + p).All(this.IsInBounds)) this.PlacingPosition = nextPos; } else { // move the camera camera.Position -= drag.Delta / camera.ActualScale; } } else if (this.PlacingAttraction != null) { foreach (var touch in MlemGame.Input.TouchState) { if (touch.State != TouchLocationState.Pressed) continue; // when first pressing down, go into attraction drag mode if we're touching the place location var offset = (camera.ToWorldPos(touch.Position) / Assets.TileSize).ToPoint(); this.draggingAttraction = this.PlacingAttraction.Type.GetCoveredTiles() .Any(p => this.PlacingPosition + p == offset); } } else { // we're not placing an attraction, so we're in remove and move mode if (MlemGame.Input.GetGesture(GestureType.Tap, out var tap) && GameImpl.Instance.UiSystem.Controls.GetElementUnderPos(tap.Position) == null) { var pos = (camera.ToWorldPos(tap.Position) / Assets.TileSize).ToPoint(); var attraction = this.GetAttractionAt(pos); if (attraction != null && (this.PlacingModifier == null || this.PlacingModifier.IsAffected(attraction))) { // actually select the top left for easy usage later this.SelectedPosition = this.attractions.First(kv => kv.Item2 == attraction).Item1; } else { this.SelectedPosition = null; } } } camera.ConstrainWorldBounds(new Vector2(-ParkMap.AdditionalRadius) * Assets.TileSize, new Vector2(this.Width + ParkMap.AdditionalRadius, this.Height + ParkMap.AdditionalRadius) * Assets.TileSize); } } public void Draw(GameTime time, SpriteBatch batch, Vector2 position, float scale, float alpha, bool showSurroundings, RectangleF visibleArea) { var tileSize = Assets.TileSize * scale; // draw ground var additionalRadius = showSurroundings ? ParkMap.AdditionalRadius : 0; var minX = Math.Max(-additionalRadius, visibleArea.Left / tileSize.X).Floor(); var minY = Math.Max(-additionalRadius, visibleArea.Top / tileSize.Y).Floor(); var maxX = Math.Min(this.Width + additionalRadius, visibleArea.Right / tileSize.X).Ceil(); var maxY = Math.Min(this.Height + additionalRadius, visibleArea.Bottom / tileSize.Y).Ceil(); for (var x = minX; x < maxX; x++) { for (var y = minY; y < maxY; y++) { var pos = new Vector2(x, y); var drawPos = position + pos * tileSize; batch.Draw(Assets.TilesTexture[0, 0], drawPos, Color.White * alpha, 0, Vector2.Zero, scale, SpriteEffects.None, 0); if (this.fencePositions.TryGetValue(pos.ToPoint(), out var fenceType)) { batch.Draw(Assets.TilesTexture[fenceType, 1], drawPos, Color.White * alpha, 0, Vector2.Zero, scale, SpriteEffects.None, 0); } else if (this.treePositions.TryGetValue(pos.ToPoint(), out var treeType)) { batch.Draw(Assets.TilesTexture[1 + treeType, 0], drawPos, Color.White * alpha, 0, Vector2.Zero, scale, SpriteEffects.None, 0); } } } // selected attraction if (this.SelectedPosition != null) { var selected = this.SelectedPosition.Value; var attr = this.GetAttractionAt(selected); foreach (var pos in attr.Type.GetCoveredTiles()) batch.Draw(batch.GetBlankTexture(), new RectangleF(position + (selected + pos).ToVector2() * tileSize, tileSize), Color.Black * 0.25F * alpha); } // draw attractions foreach (var (pos, attraction) in this.attractions) { if (this.PlacingModifier != null && this.PlacingModifier.IsAffected(attraction)) { var color = GameImpl.Instance.Tickets >= attraction.GetModifierPrice(this.PlacingModifier) ? Color.Yellow : Color.Red; foreach (var offset in attraction.Type.GetCoveredTiles()) batch.Draw(batch.GetBlankTexture(), new RectangleF(position + (pos + offset).ToVector2() * tileSize, tileSize), color * 0.25F * alpha); } attraction.Draw(batch, position + pos.ToVector2() * tileSize, alpha, scale); } // placing attraction if (this.PlacingAttraction != null) { var placingPos = position + this.PlacingPosition.ToVector2() * tileSize; var color = this.CanPlace(this.PlacingPosition, this.PlacingAttraction) ? Color.Yellow : Color.Red; foreach (var pos in this.PlacingAttraction.Type.GetCoveredTiles()) batch.Draw(batch.GetBlankTexture(), new RectangleF(placingPos + pos.ToVector2() * tileSize, tileSize), color * 0.25F * alpha); this.PlacingAttraction.Draw(batch, placingPos, alpha * 0.5F, scale); } } public bool CanPlace(Point position, Attraction attraction) { foreach (var offset in attraction.Type.GetCoveredTiles()) { if (!this.IsInBounds(position + offset)) return false; if (this.GetAttractionAt(position + offset) != null) return false; } return true; } public void Place(Point position, Attraction attraction) { foreach (var (x, y) in attraction.Type.GetCoveredTiles()) this.attractionGrid[position.X + x, position.Y + y] = attraction; this.attractions.Add((position, attraction)); } public Attraction Remove(Point position) { var attraction = this.GetAttractionAt(position); if (attraction != null) { foreach (var (x, y) in attraction.Type.GetCoveredTiles()) this.attractionGrid[position.X + x, position.Y + y] = null; this.attractions.Remove((position, attraction)); } return attraction; } public Attraction GetAttractionAt(Point position) { return !this.IsInBounds(position) ? null : this.attractionGrid[position.X, position.Y]; } public int GetAttractionAmount(AttractionType type) { return this.attractions.Count(a => type == null || a.Item2.Type == type); } public int GetModifierAmount(AttractionModifier modifier) { return this.attractions.Sum(a => a.Item2.GetModifierAmount(modifier)); } public bool IsAnyAttractionAffected(AttractionModifier modifier) { return this.attractions.Any(a => modifier.IsAffected(a.Item2)); } public IEnumerable<(Point, Attraction)> GetAttractions() { foreach (var attraction in this.attractions) yield return attraction; } public bool IsInBounds(Point pos) { return pos.X >= 0 && pos.Y >= 0 && pos.X < this.Width && pos.Y < this.Height; } public ParkMap Copy(int? newWidth = null, int? newHeight = null) { var newMap = new ParkMap(newWidth ?? this.Width, newHeight ?? this.Height); foreach (var (pos, attraction) in this.attractions) { if (newMap.CanPlace(pos, attraction)) newMap.Place(pos, attraction); } newMap.TicketsPerSecond = this.TicketsPerSecond; return newMap; } private bool TryAutoBuy() { if (!Options.Instance.AutoBuyEnabled) return false; if (GameImpl.Instance.Tickets < Options.Instance.MinTicketsForAutoBuy) return false; if (this.autoBuyCounter < ParkMap.AutoBuyIntervalSecs) return false; this.autoBuyCounter -= ParkMap.AutoBuyIntervalSecs; var success = false; // auto-buy modifiers if (Upgrade.AutoPlaceModifiers[0].IsActive()) { // loop through all attractions, but look at attractions with fewer applied modifiers first foreach (var attraction in this.attractions.Select(kv => kv.Item2).OrderBy(a => a.GetModifierAmount(null))) { var match = AttractionModifier.Modifiers.Values.Where(m => m.IsAffected(attraction)); // if we don't have level 2, we only want to increase existing modifiers if (!Upgrade.AutoPlaceModifiers[1].IsActive()) match = match.Where(m => attraction.GetModifierAmount(m) > 0); // we want to apply the least applied modifier on this attraction var modifier = match.OrderBy(m => attraction.GetModifierAmount(m)).FirstOrDefault(); if (modifier != null && modifier.Buy(attraction)) success = true; } } return success; } }