using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Graphics; using MLEM.Cameras; using MLEM.Extended.Extensions; using MLEM.Extended.Tiled; using MLEM.Extensions; using MLEM.Font; using MLEM.Misc; using MLEM.Startup; using MLEM.Textures; using MLEM.Ui; using MonoGame.Extended; using MonoGame.Extended.Tiled; using RectangleF = MLEM.Misc.RectangleF; namespace GreatSpringGameJam { public class Map { public static readonly SoundEffect ForgetMeNotPlatformSound = MlemGame.LoadContent("Sounds/PlatformAppear"); private static readonly UniformTextureAtlas BackgroundTexture = new(MlemGame.LoadContent("Textures/Backgrounds"), 8, 8); public Point SizeInPixels => new(this.map.WidthInPixels, this.map.HeightInPixels); public Point SpawnPoint => new(this.map.Properties.GetInt("StartX"), this.map.Properties.GetInt("StartY")); public Point Size => new(this.map.Width, this.map.Height); public Vector2 TileSize => this.map.GetTileSize(); public readonly int TotalSnow; public readonly int TotalSeeds; public readonly int TotalGnomes; public readonly string Name; public TimeSpan ForgetMeNotTime; private readonly TiledMap map; private readonly IndividualTiledMapRenderer renderer; private readonly TiledMapCollisions collisions; private readonly List entities = new(); public Map(TiledMap map, string name) { this.map = map; this.Name = name; this.renderer = new IndividualTiledMapRenderer(this.map); this.collisions = new TiledMapCollisions(this.map, (collisions, info) => { // snow should only "collide" with the snow blower wind if (info.Layer.Name == "Snow") return; foreach (var obj in info.TilesetTile.Objects) { // level exits shouldn't be collidable if (obj.Properties.GetBool("LevelExit")) continue; collisions.Add(obj.GetArea(info.Map, info.Position, info.Tile.Flags)); } }); this.TotalSnow = this.GetTotalTiles("Snow") / 2; this.TotalSeeds = this.GetTotalTiles("Seed"); foreach (var (pos, _) in this.EnumerateTiles("Gnome")) { this.entities.Add(new Gnome(this, new Vector2(pos.X, pos.Y))); this.SetTile(pos.Layer, pos.X, pos.Y, 0); } this.TotalGnomes = this.entities.OfType().Count(); } public void AddEntity(Entity entity) { this.entities.Add(entity); } public void RemoveEntity(Entity entity) { this.entities.Remove(entity); } public IEnumerable GetEntities() where T : Entity { return this.entities.OfType(); } public void Update(GameTime time) { this.renderer.UpdateAnimations(time); for (var i = this.entities.Count - 1; i >= 0; i--) this.entities[i].Update(time); if (this.ForgetMeNotTime > TimeSpan.Zero) { // if the time is about to run out, we play the platform disappear sound if (this.ForgetMeNotTime > ForgetMeNotPlatformSound.Duration && this.ForgetMeNotTime - time.ElapsedGameTime <= ForgetMeNotPlatformSound.Duration) ForgetMeNotPlatformSound.Play(); this.ForgetMeNotTime -= time.ElapsedGameTime; } } public TiledMapTile GetTile(string layer, int x, int y) { return this.map.GetTile(layer, x, y); } public void SetTile(string layer, int x, int y, int globalId) { var index = this.map.GetTileLayerIndex(layer); this.map.SetTile(layer, x, y, globalId); this.renderer.UpdateDrawInfo(index, x, y); this.collisions.UpdateCollisionInfo(index, x, y); } public void SetTile(string layer, int x, int y, TiledMapTileset tileset, TiledMapTilesetTile tile) { this.SetTile(layer, x, y, tile.GetGlobalIdentifier(tileset, this.map)); } public TiledMapTilesetTile GetTilesetTile(TiledMapTile tile, TiledMapTileset tileset) { return tileset.GetTilesetTile(tile, this.map); } public TiledMapTileset GetTileset(TiledMapTile tile) { return tile.GetTileset(this.map); } public RectangleF GetArea(TiledMapObject obj, Vector2? pos = null) { return obj.GetArea(this.map, pos).ToMlem(); } public void Draw(GameTime time, SpriteBatch batch, Camera camera) { batch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, transformMatrix: camera.ViewMatrix); // render background const float parallaxFactor = 0.5F; var parallax = camera.Position * parallaxFactor; var (parW, parH) = this.SizeInPixels.ToVector2() * (1 - parallaxFactor) + camera.ScaledViewport * parallaxFactor; var mountains = BackgroundTexture[0, 0, 8, 4]; for (var x = 0; x < parW; x += mountains.Width * 2) batch.Draw(mountains, parallax + new Vector2(x, parH - mountains.Height * 2), Color.White * 0.5F, 0, Vector2.Zero, 2, SpriteEffects.None, 0); var clouds = BackgroundTexture[0, 4, 8, 2]; for (var x = -2; x < parW / clouds.Width / 2; x++) { var pos = new Vector2(x * clouds.Width * 2, parH - mountains.Height * 2 - clouds.Height * 4); batch.Draw(clouds, parallax + pos + new Vector2((float) time.TotalGameTime.TotalSeconds * 3 % (clouds.Width * 2), 0), Color.White * 0.5F, 0, Vector2.Zero, 2, SpriteEffects.None, 0); batch.Draw(clouds, parallax + pos + new Vector2(clouds.Width + (float) time.TotalGameTime.TotalSeconds * 2.5F % (clouds.Width * 2), clouds.Height * 1.5F), Color.White * 0.5F, 0, Vector2.Zero, 2, SpriteEffects.None, 0); } // render map etc. this.DrawLayer(batch, "Background", camera); this.DrawLayer(batch, "Ground", camera); var ui = GameImpl.Instance.UiSystem; foreach (var obj in this.GetLevelEntrances()) { var pos = obj.GetArea(this.map).Center; var textTop = (pos - new Vector2(0, 4)) * this.TileSize; var alpha = MathHelper.Clamp(1 - (Vector2.Distance(GameImpl.Instance.Player.Position + Vector2.One / 2, pos + Vector2.One / 2) - 2) / 3, 0.25F, 1); ui.Style.Font.DrawString(batch, obj.Properties.Get("DisplayName"), textTop, TextAlign.Center, ui.Style.TextColor * alpha, 0, Vector2.Zero, 0.025F, SpriteEffects.None, 0); GameImpl.Instance.Scores.TryGetValue(obj.Name, out var score); ui.Style.Font.DrawString(batch, $"{score}% Completed", textTop + new Vector2(0, 0.75F) * this.TileSize, TextAlign.Center, ui.Style.TextColor * alpha, 0, Vector2.Zero, 0.02F, SpriteEffects.None, 0); } foreach (var entity in this.entities) entity.Draw(time, batch); this.DrawLayer(batch, "Snow", camera); if (this.ForgetMeNotTime > TimeSpan.Zero) { this.DrawLayer(batch, "ForgetMeNot", camera, (_, info) => { var alpha = Easings.OutCubic.AndReverse().Invoke((float) this.ForgetMeNotTime.TotalSeconds / 10); batch.Draw(info.Tileset.Texture, new Vector2(info.Position.X * this.map.TileWidth, info.Position.Y * this.map.TileHeight), info.Tileset.GetTextureRegion(info.TilesetTile), Color.White * alpha, 0, Vector2.Zero, 1, info.Tile.GetSpriteEffects(), info.Depth); }); } batch.End(); } public int GetTotalTiles(string type) { return this.EnumerateTiles(type).Count(); } public IEnumerable<(LayerPosition, TiledMapTilesetTile)> EnumerateTiles(string type) { for (var x = 0; x < this.map.Width; x++) { for (var y = 0; y < this.map.Height; y++) { foreach (var layer in this.map.TileLayers) { var tile = layer.GetTile(x, y); if (tile.IsBlank) continue; var tilesetTile = this.GetTilesetTile(tile, this.GetTileset(tile)); if (tilesetTile.Properties.GetBool(type)) yield return (new LayerPosition(layer.Name, x, y), tilesetTile); } } } } public IEnumerable GetLevelEntrances() { var layer = this.map.GetLayer("Entrances"); if (layer != null) { foreach (var obj in layer.Objects) yield return obj; } } public IEnumerable<(Vector2, float)> GetPenetrations(Func getArea, bool prioritizeX) { return this.collisions.GetPenetrations(() => getArea().ToExtended(), i => { if (i.Layer.Name == "ForgetMeNot") return this.ForgetMeNotTime > TimeSpan.Zero; return true; }, prioritizeX); } private void DrawLayer(SpriteBatch batch, string layer, Camera camera, IndividualTiledMapRenderer.DrawDelegate drawDelegate = null) { var id = this.map.GetTileLayerIndex(layer); if (id >= 0) this.renderer.DrawLayer(batch, id, camera.GetVisibleRectangle().ToExtended(), drawDelegate); } } }